diff --git a/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/RootGenerator.scala b/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/RootGenerator.scala index 1272031ac9..bfd28c0277 100644 --- a/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/RootGenerator.scala +++ b/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/RootGenerator.scala @@ -7,6 +7,7 @@ import sttp.tapir.codegen.openapi.models.OpenapiModels.OpenapiDocument import sttp.tapir.codegen.openapi.models.OpenapiSchemaType._ import sttp.tapir.codegen.openapi.models.SpecificationExtensionRenderer import sttp.tapir.codegen.security.SecurityGenerator +import sttp.tapir.codegen.util.ContentTypes import sttp.tapir.codegen.util.JavaEscape import sttp.tapir.codegen.util.NameHelpers import sttp.tapir.codegen.util.NameValidation @@ -243,8 +244,6 @@ object RootGenerator { } .mkString("\n") - val expectedTypes = - Set("text/plain", "text/html", "application/json", "application/xml", "multipart/form-data", "application/octet-stream") val mediaType = "([^/]+)/(.+)".r val customTypes = doc.paths .flatMap( @@ -255,7 +254,7 @@ object RootGenerator { ) .distinct .sorted - .filterNot(expectedTypes.contains) + .filterNot(ContentTypes.isNativeContentType) .map { case ct @ mediaType(mainType, subType) => s"""case class ${NameHelpers.codecFormatName(ct)}() extends CodecFormat { diff --git a/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/endpoints/EndpointGenerator.scala b/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/endpoints/EndpointGenerator.scala index 38447cbdcd..7f41828446 100644 --- a/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/endpoints/EndpointGenerator.scala +++ b/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/endpoints/EndpointGenerator.scala @@ -11,6 +11,7 @@ import sttp.tapir.codegen.openapi.models.GenerationDirectives._ import sttp.tapir.codegen.openapi.models.OpenapiModels.{OpenapiDocument, OpenapiParameter, OpenapiPath} import sttp.tapir.codegen.openapi.models.OpenapiSchemaType._ import sttp.tapir.codegen.security.{SecurityDefn, SecurityGenerator, SecurityWrapperDefn} +import sttp.tapir.codegen.util.ContentTypes import sttp.tapir.codegen.util.ErrUtils.bail import sttp.tapir.codegen.util.NameHelpers.{indent, strippedToCamelCase} import sttp.tapir.codegen.util.{JavaEscape, Location} @@ -273,12 +274,12 @@ class EndpointGenerator { .toSet val xmlParamRefs: Seq[String] = (m.requestBody.toSeq.flatMap(_.resolve(doc).content.map(c => (c.contentType, c.schema))) ++ m.responses.flatMap(_.resolve(doc).content.map(c => (c.contentType, c.schema)))) - .collect { case (contentType, schema) if contentType == "application/xml" => schema } + .collect { case (contentType, schema) if ContentTypes.isXml(contentType) => schema } .collect { case ref: OpenapiSchemaRef if ref.isSchema => ref.stripped } val jsonParamRefs = (m.requestBody.toSeq.flatMap(_.resolve(doc).content.map(c => (c.contentType, c.schema))) ++ m.responses.flatMap(_.resolve(doc).content.map(c => (c.contentType, c.schema)))) .filterNot(_ => m.tapirCodegenDirectives.contains(jsonBodyAsString)) - .collect { case (contentType, schema) if contentType == "application/json" => schema } + .collect { case (contentType, schema) if ContentTypes.isJson(contentType) => schema } .collect { case ref: OpenapiSchemaRef if ref.isSchema => ref.stripped case OpenapiSchemaArray(ref: OpenapiSchemaRef, _, _, _) if ref.isSchema => ref.stripped diff --git a/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/endpoints/InAndOutComponents.scala b/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/endpoints/InAndOutComponents.scala index b5e78ad454..188ae416bc 100644 --- a/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/endpoints/InAndOutComponents.scala +++ b/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/endpoints/InAndOutComponents.scala @@ -22,6 +22,7 @@ import sttp.tapir.codegen.openapi.models.OpenapiSchemaType.{ OpenapiSchemaSimpleType, OpenapiSchemaString } +import sttp.tapir.codegen.util.ContentTypes import sttp.tapir.codegen.util.ErrUtils.bail import sttp.tapir.codegen.util.Location import sttp.tapir.codegen.util.NameHelpers.{codecFormatName, indent, safeVariableName} @@ -47,8 +48,6 @@ object InAndOutComponents { case (None, Some(defn)) => Some(defn) case (Some(defn1), Some(defn2)) => Some(defn1 + separator + defn2) } - // These types all use 'eager' schemas, except for '*/*', which we default to eager for convenience but which has no schema mappings - private[endpoints] val eagerTypes = Set("application/json", "application/xml", "text/plain", "text/html", "multipart/form-data", "*/*") private[endpoints] def aliases(packageReuse: PackageReuseContext, types: Seq[String], seperateFilesForModels: Boolean): String = types.map(PackageReuseContext.enumAliasType(_, packageReuse, seperateFilesForModels)).mkString("\n") @@ -96,7 +95,7 @@ object InAndOutComponents { MappedContentType("stringBody", "String") case "text/html" => MappedContentType("htmlBodyUtf8", "String") - case "application/xml" if xmlSerdeLib != XmlSerdeLib.NoSupport => + case ct if ContentTypes.isXml(ct) && xmlSerdeLib != XmlSerdeLib.NoSupport => val (outT: String, maybeInline: Option[String], maybeAlias: Option[String], maybeTpe: Seq[String]) = schema match { case st: OpenapiSchemaSimpleType => val (t, _) = mapSchemaSimpleTypeToType(st) @@ -112,10 +111,10 @@ object InAndOutComponents { def toList = if (required) ".toList" else ".map(_.toList)" val bodyType = maybeAlias.map(a => s"xmlBody[$a].map(_.asInstanceOf[$req]$toList)(_.asInstanceOf[$a])").getOrElse(s"xmlBody[$req]") MappedContentType(bodyType + v(required), req, maybeInline, maybeTpe) - case "application/json" if tapirCodegenDirectives.contains(jsonBodyAsString) => + case ct if ContentTypes.isJson(ct) && tapirCodegenDirectives.contains(jsonBodyAsString) => if (required) MappedContentType("stringJsonBody", "String", None) else MappedContentType("stringJsonBody.map(Option(_))(_.orNull)", "Option[String]", None) - case "application/json" => + case ct if ContentTypes.isJson(ct) => val (outT, maybeInline) = schema match { case st: OpenapiSchemaSimpleType => val (t, _) = mapSchemaSimpleTypeToType(st) @@ -179,7 +178,7 @@ object InAndOutComponents { def eagerBody = contentType match { case "application/octet-stream" => "rawBinaryBody(sttp.tapir.RawBodyType.ByteArrayBody)" case o if o.startsWith("text/") => s"stringBodyUtf8AnyFormat(${codec("String", o)})" - case "application/xml" => s"EndpointIO.Body(RawBodyType.ByteArrayBody, CodecFormat.Xml(), EndpointIO.Info.empty)" + case o if ContentTypes.isXml(o) => s"EndpointIO.Body(RawBodyType.ByteArrayBody, CodecFormat.Xml(), EndpointIO.Info.empty)" case o => s"EndpointIO.Body(RawBodyType.ByteArrayBody, ${codec("Array[Byte]", o)}, EndpointIO.Info.empty)" } def streamingBody = contentType match { @@ -187,9 +186,9 @@ object InAndOutComponents { case "text/html" => "CodecFormat.TextHtml()" case "multipart/form-data" => "CodecFormat.MultipartFormData()" case "application/grpc" => "CodecFormat.Grpc()" - case "application/json" => "CodecFormat.Json()" + case o if ContentTypes.isJson(o) => "CodecFormat.Json()" case "application/octet-stream" => "CodecFormat.OctetStream()" - case "application/xml" => "CodecFormat.Xml()" + case o if ContentTypes.isXml(o) => "CodecFormat.Xml()" case "application/x-www-form-urlencoded" => "CodecFormat.XWwwFormUrlencoded()" case "application/zip" => "CodecFormat.Zip()" case o => s"${codecFormatName(o)}()" diff --git a/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/endpoints/InComponent.scala b/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/endpoints/InComponent.scala index 9cd8cb15c0..38d8f5cce6 100644 --- a/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/endpoints/InComponent.scala +++ b/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/endpoints/InComponent.scala @@ -1,7 +1,7 @@ package sttp.tapir.codegen.endpoints import sttp.tapir.codegen.dedup.PackageReuseContext -import sttp.tapir.codegen.endpoints.InAndOutComponents.{aliases, combine, contentTypeMapper, eagerTypes} +import sttp.tapir.codegen.endpoints.InAndOutComponents.{aliases, combine, contentTypeMapper} import sttp.tapir.codegen.endpoints.Position.Request import sttp.tapir.codegen.json.JsonSerdeLib.JsonSerdeLib import sttp.tapir.codegen.openapi.models.OpenapiModels @@ -12,7 +12,7 @@ import sttp.tapir.codegen.openapi.models.OpenapiModels.{ OpenapiRequestBodyDefn } import sttp.tapir.codegen.openapi.models.OpenapiSchemaType.OpenapiSchemaRef -import sttp.tapir.codegen.util.{JavaEscape, Location} +import sttp.tapir.codegen.util.{ContentTypes, JavaEscape, Location} import sttp.tapir.codegen.util.NameHelpers.indent import sttp.tapir.codegen.validation.ValidationDefns import sttp.tapir.codegen.xml.XmlSerdeLib.XmlSerdeLib @@ -82,7 +82,7 @@ object InComponent { Some((s".in($decl$d)", tpe, maybeInlineDefn)) } else { // We cannot mix eager and streaming types when using oneOfBody - val preferEager = b.content.exists(c => eagerTypes.contains(c.contentType)) + val preferEager = b.content.exists(c => ContentTypes.isEager(c.contentType)) val mapped = b.content.map(mapContent(_, b.required, preferEager)) val (decls, tpes, maybeInlineDefns) = mapped.unzip3 val distinctTypes = tpes.distinct diff --git a/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/endpoints/OutComponent.scala b/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/endpoints/OutComponent.scala index 6d77c3eb4a..6905d61cc8 100644 --- a/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/endpoints/OutComponent.scala +++ b/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/endpoints/OutComponent.scala @@ -15,7 +15,7 @@ import sttp.tapir.codegen.openapi.models.GenerationDirectives.{ import sttp.tapir.codegen.openapi.models.OpenapiModels.{OpenapiDocument, OpenapiResponseContent, OpenapiResponseDef} import sttp.tapir.codegen.openapi.models.OpenapiSchemaType.{OpenapiSchemaOneOf, OpenapiSchemaRef, OpenapiSchemaSimpleType} import sttp.tapir.codegen.util.ErrUtils.bail -import sttp.tapir.codegen.util.{JavaEscape, Location, NameHelpers} +import sttp.tapir.codegen.util.{ContentTypes, JavaEscape, Location, NameHelpers} import sttp.tapir.codegen.util.NameHelpers.indent import sttp.tapir.codegen.validation.ValidationDefns import sttp.tapir.codegen.xml.XmlSerdeLib.XmlSerdeLib @@ -111,7 +111,7 @@ object OutComponent { (s"$decl$d", Some(tpe), maybeInlineDefn) case seq => // We cannot mix eager and streaming types when using oneOfBody - val preferEager = seq.exists(c => eagerTypes.contains(c.contentType)) + val preferEager = seq.exists(c => ContentTypes.isEager(c.contentType)) val (decls, tpes, maybeInlineDefns) = seq.map(wrapContent(_, preferEager)).unzip3 val distinctTypes = tpes.distinct // If the types are distinct, we need to produce wrappers with a common parent for oneOfBody to work. If they're @@ -393,7 +393,6 @@ object OutComponent { .map { case (k, vs) => k -> vs.map(_._2) } .toMap val traitName = s"${endpointName.capitalize}Body${if (isErrorPosition) "Err" else "Out"}" - val mappable = Set("application/json", "application/xml", "multipart/form-data") val bodyIsStreaming = (!isErrorPosition && tapirCodegenDirectives.contains(forceRespStreaming)) || (!isErrorPosition && tapirCodegenDirectives.contains(forceStreaming)) val bodyIsEager = !bodyIsStreaming && (isErrorPosition || @@ -402,7 +401,7 @@ object OutComponent { val allElemTypes = many .flatMap(y => y.content.map(x => - (x.contentType, x.schema, y.content.size > 1 && y.content.map(_.contentType).exists(!mappable.contains(_))) + (x.contentType, x.schema, y.content.size > 1 && y.content.map(_.contentType).exists(!ContentTypes.isClassMappable(_))) ) ) .map { @@ -410,12 +409,12 @@ object OutComponent { case (_, _, true) => traitName case (ct, _, _) if ct.startsWith("text/") && isErrorPosition => "String" case ("text/plain" | "text/html", _, _) => "String" - case ("application/json", _, _) if tapirCodegenDirectives.contains(jsonBodyAsString) => "String" - case (ct, r: OpenapiSchemaRef, _) if mappable.contains(ct) => r.stripped - case (ct, x: OpenapiSchemaSimpleType, _) if mappable.contains(ct) => mapSchemaSimpleTypeToType(x)._1 - case (ct, x, _) if mappable.contains(ct) => bail(s"Unexpected oneOf elem type $x with content type $ct") - case (_, _, _) if bodyIsEager => "Array[Byte]" - case (_, _, _) => capabilityType(streamingImplementation) + case (ct, _, _) if ContentTypes.isJson(ct) && tapirCodegenDirectives.contains(jsonBodyAsString) => "String" + case (ct, r: OpenapiSchemaRef, _) if ContentTypes.isClassMappable(ct) => r.stripped + case (ct, x: OpenapiSchemaSimpleType, _) if ContentTypes.isClassMappable(ct) => mapSchemaSimpleTypeToType(x)._1 + case (ct, x, _) if ContentTypes.isClassMappable(ct) => bail(s"Unexpected oneOf elem type $x with content type $ct") + case (_, _, _) if bodyIsEager => "Array[Byte]" + case (_, _, _) => capabilityType(streamingImplementation) } .distinct val commmonType = { diff --git a/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/json/JsonHelpers.scala b/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/json/JsonHelpers.scala index 2148d1d6c9..1965e98dbe 100644 --- a/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/json/JsonHelpers.scala +++ b/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/json/JsonHelpers.scala @@ -15,6 +15,8 @@ import sttp.tapir.codegen.openapi.models.OpenapiSchemaType.{ OpenapiSchemaStringType } +import sttp.tapir.codegen.util.ContentTypes + import scala.annotation.tailrec object JsonHelpers { @@ -91,12 +93,12 @@ object JsonHelpers { m.responses .map(_.resolve(doc)) .flatMap(_.content) - .filter(o => o.contentType == "application/json" && o.schema.isInstanceOf[OpenapiSchemaObject]) + .filter(o => ContentTypes.isJson(o.contentType) && o.schema.isInstanceOf[OpenapiSchemaObject]) .map(c => (m.name(p.url).capitalize + "Response", c.schema, true)) ++ m.requestBody.toSeq .map(_.resolve(doc)) .flatMap(_.content) - .filter(o => o.contentType == "application/json" && o.schema.isInstanceOf[OpenapiSchemaObject]) + .filter(o => ContentTypes.isJson(o.contentType) && o.schema.isInstanceOf[OpenapiSchemaObject]) .map(c => (m.name(p.url).capitalize + "Request", c.schema, true)) ) ) diff --git a/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/util/ContentTypes.scala b/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/util/ContentTypes.scala new file mode 100644 index 0000000000..8eaa804e45 --- /dev/null +++ b/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/util/ContentTypes.scala @@ -0,0 +1,39 @@ +package sttp.tapir.codegen.util + +object ContentTypes { + private val jsonContentType = "application/(.+\\+)?json".r + + /** Returns if the content type is a Json content type. Accepts standard application/json, as well as extended application/foo+json + * content types + */ + def isJson(contentType: String): Boolean = jsonContentType.pattern.matcher(contentType).matches + + private val xmlContentType = "application/(.+\\+)?xml".r + + /** Returns if the content type is a XML content type. Accepts standard application/xml, as well as extended application/foo+xml content + * types + */ + def isXml(contentType: String): Boolean = xmlContentType.pattern.matcher(contentType).matches + + private val nativeContentTypes: Set[String] = Set("text/plain", "text/html", "multipart/form-data", "application/octet-stream") + + /** Returns true if the content type has a native codec in codegen, and does not require a custom codec + */ + def isNativeContentType(contentType: String): Boolean = + nativeContentTypes.contains(contentType) || isJson(contentType) || isXml(contentType) + + private val classMappableContentTypes = Set("multipart/form-data") + + /** Returns true if a content type can be mapped to a scala class + */ + def isClassMappable(contentType: String): Boolean = + classMappableContentTypes.contains(contentType) || ContentTypes.isJson(contentType) || ContentTypes.isXml(contentType) + + // These types all use 'eager' schemas, except for '*/*', which we default to eager for convenience but which has no schema mappings + private val eagerContentTypes = Set("text/plain", "text/html", "multipart/form-data", "*/*") + + /** Returns true if the content type is eager + */ + def isEager(contentType: String): Boolean = + eagerContentTypes.contains(contentType) || ContentTypes.isJson(contentType) || ContentTypes.isXml(contentType) +} diff --git a/openapi-codegen/core/src/test/scala/sttp/tapir/codegen/RootGeneratorSpec.scala b/openapi-codegen/core/src/test/scala/sttp/tapir/codegen/RootGeneratorSpec.scala index ad0832d193..125daf7cfe 100644 --- a/openapi-codegen/core/src/test/scala/sttp/tapir/codegen/RootGeneratorSpec.scala +++ b/openapi-codegen/core/src/test/scala/sttp/tapir/codegen/RootGeneratorSpec.scala @@ -125,6 +125,29 @@ class RootGeneratorSpec extends CompileCheckTestBase { generated.shouldCompile() }) + VersionCheck.runTest(jsonSerdeLib)( + it should s"treat 'application/xxx+json' bodies as json using $jsonSerdeLib serdes" in { + val doc = TestHelpers.parseYamlDocument(TestHelpers.structuredSyntaxSuffixJsonYaml).fold(err => fail(err.getMessage), identity) + val generated = gen(doc, useHeadTagForObjectNames = false, jsonSerdeLib = jsonSerdeLib) + + // 'application/problem+json' error body + generated should include("""errorOut(jsonBody[Problem]""") + // 'application/merge-patch+json' request body, and 'application/vnd.example.widget+json' response body + generated should include( + """ lazy val patchWidget = + | endpoint + | .name("patchWidget") + | .patch + | .in(("widgets" / path[String]("id"))) + | .in(jsonBody[Widget]) + | .out(jsonBody[List[Widget]].description(""))""".stripMargin + ) + // '+json' types are mapped to jsonBody, so need no generated CodecFormat + generated should not include "extends CodecFormat" + generated.shouldCompile() + } + ) + VersionCheck.runTest(jsonSerdeLib)( it should s"compile endpoints with date and duration default values using ${jsonSerdeLib} serdes" in { val doc = TestHelpers.parseYamlDocument(TestHelpers.dateAndDurationDefaultsYaml).fold(err => fail(err.getMessage), identity) @@ -138,6 +161,34 @@ class RootGeneratorSpec extends CompileCheckTestBase { ) } + it should "treat 'application/xxx+xml' bodies as xml" in { + val doc = TestHelpers.parseYamlDocument(TestHelpers.structuredSyntaxSuffixXmlYaml).fold(err => fail(err.getMessage), identity) + val generated = genMap(doc, useHeadTagForObjectNames = false, jsonSerdeLib = "circe") + val endpoints = generated("TapirGeneratedEndpoints") + + // 'application/vnd.example.gadget+xml' request body, 'application/atom+xml' response body and + // 'application/problem+xml' error body + endpoints should include( + """ lazy val createGadget = + | endpoint + | .name("createGadget") + | .post + | .in(("gadgets")) + | .in(xmlBody[Gadget]) + | .errorOut(xmlBody[Fault].description("").and(statusCode(sttp.model.StatusCode(400)))) + | .out(xmlBody[Gadget].description(""))""".stripMargin + ) + // '+xml' types are mapped to xmlBody, so need no generated CodecFormat + endpoints should not include "extends CodecFormat" + + // xml serdes must be generated for types only ever referenced from a '+xml' body + val xmlSerdes = generated("TapirGeneratedEndpointsXmlSerdes") + xmlSerdes should include("Decoder[Gadget]") + xmlSerdes should include("Encoder[Gadget]") + xmlSerdes should include("Decoder[Fault]") + xmlSerdes should include("Encoder[Fault]") + } + it should "split models into separate files when seperateFilesForModels is true" in { val info = RootGenerator.generateObjects( TestHelpers.myBookshopDoc, diff --git a/openapi-codegen/core/src/test/scala/sttp/tapir/codegen/TestHelpers.scala b/openapi-codegen/core/src/test/scala/sttp/tapir/codegen/TestHelpers.scala index 1a0bc7da4d..be856cb478 100644 --- a/openapi-codegen/core/src/test/scala/sttp/tapir/codegen/TestHelpers.scala +++ b/openapi-codegen/core/src/test/scala/sttp/tapir/codegen/TestHelpers.scala @@ -805,6 +805,138 @@ object TestHelpers { | default: PT1H30M |""".stripMargin + // 'application/problem+json' (RFC 9457) and any other 'application/+json' media type should be handled + // exactly like 'application/json' + val structuredSyntaxSuffixJsonYaml = + """ + |openapi: 3.1.0 + |info: + | title: structured syntax suffix json test + | version: '1.0' + |paths: + | /widgets: + | post: + | operationId: createWidget + | requestBody: + | required: true + | content: + | application/json: + | schema: + | $ref: '#/components/schemas/Widget' + | responses: + | '200': + | description: '' + | content: + | application/json: + | schema: + | $ref: '#/components/schemas/Widget' + | '400': + | description: '' + | content: + | application/problem+json: + | schema: + | $ref: '#/components/schemas/Problem' + | /widgets/{id}: + | patch: + | operationId: patchWidget + | parameters: + | - name: id + | in: path + | required: true + | schema: + | type: string + | requestBody: + | required: true + | content: + | application/merge-patch+json: + | schema: + | $ref: '#/components/schemas/Widget' + | responses: + | '200': + | description: '' + | content: + | application/vnd.example.widget+json: + | schema: + | type: array + | items: + | $ref: '#/components/schemas/Widget' + |components: + | schemas: + | Widget: + | required: + | - name + | type: object + | properties: + | name: + | type: string + | colour: + | type: string + | Problem: + | required: + | - type + | - title + | type: object + | properties: + | type: + | type: string + | title: + | type: string + | status: + | type: integer + | format: int32 + |""".stripMargin + + // 'application/problem+xml' (RFC 9457) and any other 'application/+xml' media type should be handled + // exactly like 'application/xml' + val structuredSyntaxSuffixXmlYaml = + """ + |openapi: 3.1.0 + |info: + | title: structured syntax suffix xml test + | version: '1.0' + |paths: + | /gadgets: + | post: + | operationId: createGadget + | requestBody: + | required: true + | content: + | application/vnd.example.gadget+xml: + | schema: + | $ref: '#/components/schemas/Gadget' + | responses: + | '200': + | description: '' + | content: + | application/atom+xml: + | schema: + | $ref: '#/components/schemas/Gadget' + | '400': + | description: '' + | content: + | application/problem+xml: + | schema: + | $ref: '#/components/schemas/Fault' + |components: + | schemas: + | Gadget: + | required: + | - name + | type: object + | properties: + | name: + | type: string + | colour: + | type: string + | Fault: + | required: + | - reason + | type: object + | properties: + | reason: + | type: string + |""".stripMargin + val withDefaultsYaml = """ |openapi: 3.1.0