Skip to content

Commit e45c720

Browse files
BITespressoMichael Bergeradamw
authored
Fix const keyword being used in OpenAPI 3.0.3 (#241) (#242)
Co-authored-by: Michael Berger <[email protected]> Co-authored-by: adamw <[email protected]>
1 parent b6de18c commit e45c720

File tree

6 files changed

+83
-10
lines changed

6 files changed

+83
-10
lines changed

jsonschema-circe/src/main/scala/sttp/apispec/internal/JsonSchemaCirceEncoders.scala

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ trait JsonSchemaCirceEncoders {
2121
implicit lazy val encoderSchema: Encoder[Schema] = Encoder.AsObject.instance { (s: Schema) =>
2222
val nullSchema = Schema(`type` = Some(List(SchemaType.Null)))
2323

24+
// In OpenAPI 3.0 the keyword "const" is not allowed and needs to be replaced by an "enum" with one element
25+
val enumAndConstFields =
26+
if (openApi30 && s.const.isDefined)
27+
Vector("enum" := s.const.map(List(_)), "const" := None)
28+
else
29+
Vector("enum" := s.`enum`, "const" := s.const)
30+
2431
// Nullable $ref Schema is represented as {"anyOf": [{"$ref": "some-ref"}, {"type": "null"}]}
2532
// In OpenAPI 3.0, we need to translate it to {"allOf": [{"$ref": "some-ref"}], "nullable": true}
2633
val wrappedNullableRef30 = s.anyOf match {
@@ -31,13 +38,13 @@ trait JsonSchemaCirceEncoders {
3138

3239
val typeAndNullable = s.`type` match {
3340
case Some(List(tpe)) =>
34-
List("type" := tpe)
41+
Vector("type" := tpe)
3542
case Some(List(tpe, SchemaType.Null)) if openApi30 =>
36-
List("type" := tpe, "nullable" := true)
43+
Vector("type" := tpe, "nullable" := true)
3744
case None if wrappedNullableRef30.isDefined =>
38-
List("nullable" := true)
45+
Vector("nullable" := true)
3946
case t =>
40-
List("type" := t)
47+
Vector("type" := t)
4148
}
4249

4350
val minFields = (s.minimum, s.exclusiveMinimum) match {
@@ -78,9 +85,7 @@ trait JsonSchemaCirceEncoders {
7885
"deprecated" := s.deprecated,
7986
"readOnly" := s.readOnly,
8087
"writeOnly" := s.writeOnly
81-
) ++ exampleFields ++ typeAndNullable ++ Vector(
82-
"enum" := s.`enum`,
83-
"const" := s.const,
88+
) ++ exampleFields ++ typeAndNullable ++ enumAndConstFields ++ Vector(
8489
"format" := s.format,
8590
"allOf" := wrappedNullableRef30.map(List(_)).getOrElse(s.allOf),
8691
"anyOf" := (if (wrappedNullableRef30.isDefined) Nil else s.anyOf),
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"openapi": "3.0.1",
3+
"info": {
4+
"title": "API",
5+
"version": "1.0.0"
6+
},
7+
"components": {
8+
"schemas": {
9+
"const and enum" : {
10+
"description" : "const and enum",
11+
"enum" : [
12+
"const1"
13+
]
14+
}
15+
}
16+
}
17+
}

openapi-circe/src/test/resources/spec/3.0/schema.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,19 @@
5858
"maximum": 20,
5959
"description": "min/max"
6060
},
61+
"const" : {
62+
"description" : "const",
63+
"enum" : [
64+
"const1"
65+
]
66+
},
67+
"enum" : {
68+
"description" : "enum",
69+
"enum" : [
70+
"enum1",
71+
"enum2"
72+
]
73+
},
6174
"exclusive min/max": {
6275
"minimum": 10,
6376
"exclusiveMinimum": true,

openapi-circe/src/test/resources/spec/3.1/schema.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,17 @@
6868
"maximum": 20,
6969
"description": "min/max"
7070
},
71+
"const" : {
72+
"const" : "const1",
73+
"description" : "const"
74+
},
75+
"enum" : {
76+
"description" : "enum",
77+
"enum" : [
78+
"enum1",
79+
"enum2"
80+
]
81+
},
7182
"exclusive min/max": {
7283
"exclusiveMinimum": 10,
7384
"exclusiveMaximum": 20,

openapi-circe/src/test/scala/sttp/apispec/openapi/circe/DecoderTest.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,14 @@ class DecoderTest extends AnyFunSuite with ResourcePlatform {
4242
val Right(openapi) = readJson("/spec/3.1/schema.json").flatMap(_.as[OpenAPI]): @unchecked
4343
assert(openapi.info.title === "API")
4444
val schemas = openapi.components.getOrElse(Components.Empty).schemas
45-
assert(schemas.size === 13)
45+
assert(schemas.size === 15)
4646
}
4747

4848
test("all schemas types 3.0") {
4949
val Right(openapi) = readJson("/spec/3.0/schema.json").flatMap(_.as[OpenAPI]): @unchecked
5050
assert(openapi.info.title === "API")
5151
val schemas = openapi.components.getOrElse(Components.Empty).schemas
52-
assert(schemas.size === 12)
52+
assert(schemas.size === 14)
5353
}
5454

5555
test("decode security scheme with not empty scopes") {

openapi-circe/src/test/scala/sttp/apispec/openapi/circe/threeone/EncoderTest.scala

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,9 @@ class EncoderTest extends AnyFunSuite with ResourcePlatform {
121121
),
122122
schemaComponent("exclusiveMinimum false")(Schema(minimum = Some(BigDecimal(10)))),
123123
schemaComponent("array")(Schema(SchemaType.Array).copy(items = Some(Schema(SchemaType.String)))),
124-
schemaComponent("array with unique items")(Schema(SchemaType.Array).copy(uniqueItems = Some(true)))
124+
schemaComponent("array with unique items")(Schema(SchemaType.Array).copy(uniqueItems = Some(true))),
125+
schemaComponent("const")(Schema(const = Some("const1").map(ExampleSingleValue(_)))),
126+
schemaComponent("enum")(Schema(`enum` = Some(List("enum1", "enum2").map(ExampleSingleValue(_)))))
125127
)
126128
)
127129

@@ -160,6 +162,31 @@ class EncoderTest extends AnyFunSuite with ResourcePlatform {
160162
assert(openApiJson.spaces2SortKeys == json.spaces2SortKeys)
161163
}
162164

165+
test("replace const by single enum value in 3.0 schema") {
166+
import sttp.apispec.openapi.circe_openapi_3_0_3._
167+
168+
val components = Components(
169+
schemas = ListMap(
170+
schemaComponent("const and enum")(
171+
Schema(
172+
const = Some("const1").map(ExampleSingleValue(_)),
173+
`enum` = Some(List("enum1", "enum2").map(ExampleSingleValue(_)))
174+
)
175+
)
176+
)
177+
)
178+
179+
val openApiJson = fullSchemaOpenApi
180+
.copy(
181+
openapi = "3.0.1",
182+
components = Some(components)
183+
)
184+
.asJson
185+
val Right(json) = readJson("/spec/3.0/const_and_enum.json"): @unchecked
186+
187+
assert(openApiJson.spaces2SortKeys == json.spaces2SortKeys)
188+
}
189+
163190
private def clientCredentialsSecurityScheme(scopesRequirement: ListMap[String, String]): SecurityScheme =
164191
SecurityScheme(
165192
`type` = "oauth2",

0 commit comments

Comments
 (0)