diff --git a/jsonschema-circe/src/main/scala/sttp/apispec/internal/JsonSchemaCirceEncoders.scala b/jsonschema-circe/src/main/scala/sttp/apispec/internal/JsonSchemaCirceEncoders.scala index 700ac0b..5544b42 100644 --- a/jsonschema-circe/src/main/scala/sttp/apispec/internal/JsonSchemaCirceEncoders.scala +++ b/jsonschema-circe/src/main/scala/sttp/apispec/internal/JsonSchemaCirceEncoders.scala @@ -21,6 +21,13 @@ trait JsonSchemaCirceEncoders { implicit lazy val encoderSchema: Encoder[Schema] = Encoder.AsObject.instance { (s: Schema) => val nullSchema = Schema(`type` = Some(List(SchemaType.Null))) + // In OpenAPI 3.0 the keyword "const" is not allowed and needs to be replaced by an "enum" with one element + val enumAndConstFields = + if (openApi30 && s.const.isDefined) + Vector("enum" := s.const.map(List(_)), "const" := None) + else + Vector("enum" := s.`enum`, "const" := s.const) + // Nullable $ref Schema is represented as {"anyOf": [{"$ref": "some-ref"}, {"type": "null"}]} // In OpenAPI 3.0, we need to translate it to {"allOf": [{"$ref": "some-ref"}], "nullable": true} val wrappedNullableRef30 = s.anyOf match { @@ -31,13 +38,13 @@ trait JsonSchemaCirceEncoders { val typeAndNullable = s.`type` match { case Some(List(tpe)) => - List("type" := tpe) + Vector("type" := tpe) case Some(List(tpe, SchemaType.Null)) if openApi30 => - List("type" := tpe, "nullable" := true) + Vector("type" := tpe, "nullable" := true) case None if wrappedNullableRef30.isDefined => - List("nullable" := true) + Vector("nullable" := true) case t => - List("type" := t) + Vector("type" := t) } val minFields = (s.minimum, s.exclusiveMinimum) match { @@ -78,9 +85,7 @@ trait JsonSchemaCirceEncoders { "deprecated" := s.deprecated, "readOnly" := s.readOnly, "writeOnly" := s.writeOnly - ) ++ exampleFields ++ typeAndNullable ++ Vector( - "enum" := s.`enum`, - "const" := s.const, + ) ++ exampleFields ++ typeAndNullable ++ enumAndConstFields ++ Vector( "format" := s.format, "allOf" := wrappedNullableRef30.map(List(_)).getOrElse(s.allOf), "anyOf" := (if (wrappedNullableRef30.isDefined) Nil else s.anyOf), diff --git a/openapi-circe/src/test/resources/spec/3.0/const_and_enum.json b/openapi-circe/src/test/resources/spec/3.0/const_and_enum.json new file mode 100644 index 0000000..0fbdbd7 --- /dev/null +++ b/openapi-circe/src/test/resources/spec/3.0/const_and_enum.json @@ -0,0 +1,17 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "API", + "version": "1.0.0" + }, + "components": { + "schemas": { + "const and enum" : { + "description" : "const and enum", + "enum" : [ + "const1" + ] + } + } + } +} diff --git a/openapi-circe/src/test/resources/spec/3.0/schema.json b/openapi-circe/src/test/resources/spec/3.0/schema.json index a2ace49..c1366b9 100644 --- a/openapi-circe/src/test/resources/spec/3.0/schema.json +++ b/openapi-circe/src/test/resources/spec/3.0/schema.json @@ -58,6 +58,19 @@ "maximum": 20, "description": "min/max" }, + "const" : { + "description" : "const", + "enum" : [ + "const1" + ] + }, + "enum" : { + "description" : "enum", + "enum" : [ + "enum1", + "enum2" + ] + }, "exclusive min/max": { "minimum": 10, "exclusiveMinimum": true, diff --git a/openapi-circe/src/test/resources/spec/3.1/schema.json b/openapi-circe/src/test/resources/spec/3.1/schema.json index 7cd7391..bc650b8 100644 --- a/openapi-circe/src/test/resources/spec/3.1/schema.json +++ b/openapi-circe/src/test/resources/spec/3.1/schema.json @@ -68,6 +68,17 @@ "maximum": 20, "description": "min/max" }, + "const" : { + "const" : "const1", + "description" : "const" + }, + "enum" : { + "description" : "enum", + "enum" : [ + "enum1", + "enum2" + ] + }, "exclusive min/max": { "exclusiveMinimum": 10, "exclusiveMaximum": 20, diff --git a/openapi-circe/src/test/scala/sttp/apispec/openapi/circe/DecoderTest.scala b/openapi-circe/src/test/scala/sttp/apispec/openapi/circe/DecoderTest.scala index b9d4aac..e7fa135 100644 --- a/openapi-circe/src/test/scala/sttp/apispec/openapi/circe/DecoderTest.scala +++ b/openapi-circe/src/test/scala/sttp/apispec/openapi/circe/DecoderTest.scala @@ -42,14 +42,14 @@ class DecoderTest extends AnyFunSuite with ResourcePlatform { val Right(openapi) = readJson("/spec/3.1/schema.json").flatMap(_.as[OpenAPI]): @unchecked assert(openapi.info.title === "API") val schemas = openapi.components.getOrElse(Components.Empty).schemas - assert(schemas.size === 13) + assert(schemas.size === 15) } test("all schemas types 3.0") { val Right(openapi) = readJson("/spec/3.0/schema.json").flatMap(_.as[OpenAPI]): @unchecked assert(openapi.info.title === "API") val schemas = openapi.components.getOrElse(Components.Empty).schemas - assert(schemas.size === 12) + assert(schemas.size === 14) } test("decode security scheme with not empty scopes") { diff --git a/openapi-circe/src/test/scala/sttp/apispec/openapi/circe/threeone/EncoderTest.scala b/openapi-circe/src/test/scala/sttp/apispec/openapi/circe/threeone/EncoderTest.scala index 3c3d631..0442c26 100644 --- a/openapi-circe/src/test/scala/sttp/apispec/openapi/circe/threeone/EncoderTest.scala +++ b/openapi-circe/src/test/scala/sttp/apispec/openapi/circe/threeone/EncoderTest.scala @@ -121,7 +121,9 @@ class EncoderTest extends AnyFunSuite with ResourcePlatform { ), schemaComponent("exclusiveMinimum false")(Schema(minimum = Some(BigDecimal(10)))), schemaComponent("array")(Schema(SchemaType.Array).copy(items = Some(Schema(SchemaType.String)))), - schemaComponent("array with unique items")(Schema(SchemaType.Array).copy(uniqueItems = Some(true))) + schemaComponent("array with unique items")(Schema(SchemaType.Array).copy(uniqueItems = Some(true))), + schemaComponent("const")(Schema(const = Some("const1").map(ExampleSingleValue(_)))), + schemaComponent("enum")(Schema(`enum` = Some(List("enum1", "enum2").map(ExampleSingleValue(_))))) ) ) @@ -160,6 +162,31 @@ class EncoderTest extends AnyFunSuite with ResourcePlatform { assert(openApiJson.spaces2SortKeys == json.spaces2SortKeys) } + test("replace const by single enum value in 3.0 schema") { + import sttp.apispec.openapi.circe_openapi_3_0_3._ + + val components = Components( + schemas = ListMap( + schemaComponent("const and enum")( + Schema( + const = Some("const1").map(ExampleSingleValue(_)), + `enum` = Some(List("enum1", "enum2").map(ExampleSingleValue(_))) + ) + ) + ) + ) + + val openApiJson = fullSchemaOpenApi + .copy( + openapi = "3.0.1", + components = Some(components) + ) + .asJson + val Right(json) = readJson("/spec/3.0/const_and_enum.json"): @unchecked + + assert(openApiJson.spaces2SortKeys == json.spaces2SortKeys) + } + private def clientCredentialsSecurityScheme(scopesRequirement: ListMap[String, String]): SecurityScheme = SecurityScheme( `type` = "oauth2",