Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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),
Expand Down
17 changes: 17 additions & 0 deletions openapi-circe/src/test/resources/spec/3.0/const_and_enum.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
}
}
}
13 changes: 13 additions & 0 deletions openapi-circe/src/test/resources/spec/3.0/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
11 changes: 11 additions & 0 deletions openapi-circe/src/test/resources/spec/3.1/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(_)))))
)
)

Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this correctly rendered, since in components we define both a const and an enum, and the const_and_enum.json file contains only one enum?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason is that in order to get rid of the const we need to replace the enum. As the Schema itself does not forbid to have a const and an enum defined for the same item (which of course makes not sense) we will just overwrite the enum with the value defined in const. Hence, what were two different things in the Schema (const and enum) will be reduced to just an enum in the JSON.

Does that make sense?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or explained by example:

For this schemaComponent

schemaComponent("const and enum")(
  Schema(
    const = Some("const1").map(ExampleSingleValue(_)),
    `enum` = Some(List("enum1", "enum2").map(ExampleSingleValue(_)))
  )
)

the following would be the JSON for OpenAPI 3.1.0 (although it does not make sense to have something being an enum and const at the same time):

{
  "openapi" : "3.1.0",
  "info" : {
    "title" : "API",
    "version" : "1.0.0"
  },
  "components" : {
    "schemas" : {
      "const and enum" : {
        "description" : "const and enum",
        "enum" : [
          "enum1",
          "enum2"
        ],
        "const" : "const1"
      }
    }
  }
}

for OpenAPI 3.0.x this will be transformed to:

{
  "openapi" : "3.0.1",
  "info" : {
    "title" : "API",
    "version" : "1.0.0"
  },
  "components" : {
    "schemas" : {
      "const and enum" : {
        "description" : "const and enum",
        "enum" : [
          "const1"
        ]
      }
    }
  }
}

The above is only an edge case test. In reality it is more important to have the correct result for the "full 3.0 schema" and "full 3.1 schema" tests.

in "/spec/3.1/schema.json"

      "const" : {
        "const" : "const1",
        "description" : "const"
      },

versus in "/spec/3.0/schema.json"

      "const" : {
        "description" : "const",
        "enum" : [
          "const1"
        ]
      }

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok the edge case mislead me :)


assert(openApiJson.spaces2SortKeys == json.spaces2SortKeys)
}

private def clientCredentialsSecurityScheme(scopesRequirement: ListMap[String, String]): SecurityScheme =
SecurityScheme(
`type` = "oauth2",
Expand Down