Skip to content

Commit

Permalink
[OpenAPI] case class field being an Option of an ADT are not rendered…
Browse files Browse the repository at this point in the history
… as nullable
  • Loading branch information
guizmaii committed Aug 26, 2024
1 parent a8b432d commit 036f03f
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,11 @@ object OpenAPIGenSpec extends ZIOSpecDefault {
implicit def schema[T: Schema]: Schema[WithGenericPayload[T]] = DeriveSchema.gen
}

final case class WithOptionalAdtPayload(optionalAdtField: Option[SealedTraitCustomDiscriminator])
object WithOptionalAdtPayload {
implicit val schema: Schema[WithOptionalAdtPayload] = DeriveSchema.gen
}

private val simpleEndpoint =
Endpoint(
(GET / "static" / int("id") / uuid("uuid") ?? Doc.p("user id") / string("name")) ?? Doc.p("get path"),
Expand Down Expand Up @@ -2231,9 +2236,9 @@ object OpenAPIGenSpec extends ZIOSpecDefault {
| "discriminator" : {
| "propertyName" : "type",
| "mapping" : {
| "One" : "#/components/schemas/One}",
| "Two" : "#/components/schemas/Two}",
| "three" : "#/components/schemas/Three}"
| "One" : "#/components/schemas/One",
| "Two" : "#/components/schemas/Two",
| "three" : "#/components/schemas/Three"
| }
| }
| },
Expand Down Expand Up @@ -2810,7 +2815,10 @@ object OpenAPIGenSpec extends ZIOSpecDefault {
| ]
| },
| "nestedOption" : {
| "$ref" : "#/components/schemas/Recursive"
| "anyOf" : [
| { "type" : "null" },
| { "$ref" : "#/components/schemas/Recursive" }
| ]
| },
| "nestedMap" : {
| "type" : "object",
Expand Down Expand Up @@ -2989,6 +2997,114 @@ object OpenAPIGenSpec extends ZIOSpecDefault {
|""".stripMargin
assertTrue(json == toJsonAst(expectedJson))
},
test("Optional ADT payload") {
val endpoint = Endpoint(GET / "static").in[WithOptionalAdtPayload]
val generated = OpenAPIGen.fromEndpoints("Simple Endpoint", "1.0", endpoint)
val json = toJsonAst(generated)
val expectedJson =
"""{
| "openapi" : "3.1.0",
| "info" : {
| "title" : "Simple Endpoint",
| "version" : "1.0"
| },
| "paths" : {
| "/static" : {
| "get" : {
| "requestBody" :
| {
| "content" : {
| "application/json" : {
| "schema" :
| {
| "$ref" : "#/components/schemas/WithOptionalAdtPayload"
| }
| }
| },
| "required" : true
| }
| }
| }
| },
| "components" : {
| "schemas" : {
| "One" :
| {
| "type" :
| "object",
| "properties" : {}
| },
| "SealedTraitCustomDiscriminator" :
| {
| "oneOf" : [
| {
| "$ref" : "#/components/schemas/One"
| },
| {
| "$ref" : "#/components/schemas/Two"
| },
| {
| "$ref" : "#/components/schemas/Three"
| }
| ],
| "discriminator" : {
| "propertyName" : "type",
| "mapping" : {
| "One" : "#/components/schemas/One",
| "Two" : "#/components/schemas/Two",
| "three" : "#/components/schemas/Three"
| }
| }
| },
| "WithOptionalAdtPayload" :
| {
| "type" :
| "object",
| "properties" : {
| "optionalAdtField" : {
| "anyOf": [
| { "type": "null" },
| { "$ref": "#/components/schemas/SealedTraitCustomDiscriminator" }
| ]
| }
| },
| "required" : [
| "optionalAdtField"
| ]
| },
| "Two" :
| {
| "type" :
| "object",
| "properties" : {
| "name" : {
| "type" :
| "string"
| }
| },
| "required" : [
| "name"
| ]
| },
| "Three" :
| {
| "type" :
| "object",
| "properties" : {
| "name" : {
| "type" :
| "string"
| }
| },
| "required" : [
| "name"
| ]
| }
| }
| }
|}""".stripMargin
assertTrue(json == toJsonAst(expectedJson))
},
)

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import zio.schema.codec._
import zio.schema.codec.json._
import zio.schema.validation._

import zio.http.codec.{PathCodec, SegmentCodec, TextCodec}
import zio.http.codec.{SegmentCodec, TextCodec}

@nowarn("msg=possible missing interpolator")
private[openapi] case class SerializableJsonSchema(
Expand Down Expand Up @@ -47,23 +47,35 @@ private[openapi] case class SerializableJsonSchema(
uniqueItems: Option[Boolean] = None,
minItems: Option[Int] = None,
) {
def asNullableType(nullable: Boolean): SerializableJsonSchema =
def asNullableType(nullable: Boolean): SerializableJsonSchema = {
import SerializableJsonSchema.typeNull

if (nullable && schemaType.isDefined)
copy(schemaType = Some(schemaType.get.add("null")))
else if (nullable && oneOf.isDefined)
copy(oneOf = Some(oneOf.get :+ SerializableJsonSchema(schemaType = Some(TypeOrTypes.Type("null")))))
copy(oneOf = Some(oneOf.get :+ typeNull))
else if (nullable && allOf.isDefined)
SerializableJsonSchema(allOf =
Some(Chunk(this, SerializableJsonSchema(schemaType = Some(TypeOrTypes.Type("null"))))),
)
SerializableJsonSchema(allOf = Some(Chunk(this, typeNull)))
else if (nullable && anyOf.isDefined)
copy(anyOf = Some(anyOf.get :+ SerializableJsonSchema(schemaType = Some(TypeOrTypes.Type("null")))))
copy(anyOf = Some(anyOf.get :+ typeNull))
else if (nullable && ref.isDefined && default.isEmpty)
SerializableJsonSchema(anyOf = Some(Chunk(typeNull, this)))
else
this

}
}

private[openapi] object SerializableJsonSchema {

/**
* Used to generate a OpenAPI schema part looking like this:
* {{{
* { "type": "null"}
* }}}
*/
private[SerializableJsonSchema] val typeNull: SerializableJsonSchema =
SerializableJsonSchema(schemaType = Some(TypeOrTypes.Type("null")))

implicit val doubleOrLongSchema: Schema[Either[Double, Long]] =
Schema.fallback(Schema[Double], Schema[Long]).transform(_.toEither, Fallback.fromEither)

Expand Down Expand Up @@ -766,7 +778,8 @@ object JsonSchema {
case nominal: TypeId.Nominal if referenceType == SchemaStyle.Reference =>
Some(s"#/components/schemas/${nominal.fullyQualified.replace(".", "_")}")
case nominal: TypeId.Nominal if referenceType == SchemaStyle.Compact =>
Some(s"#/components/schemas/${nominal.typeName}")
val toto = s"#/components/schemas/${nominal.typeName}"
Some(toto)
case _ =>
None
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -988,7 +988,7 @@ object OpenAPIGen {
}
private def schemaReferencePath(nominal: TypeId.Nominal, referenceType: SchemaStyle): String = {
referenceType match {
case SchemaStyle.Compact => s"#/components/schemas/${nominal.typeName}}"
case SchemaStyle.Compact => s"#/components/schemas/${nominal.typeName}"
case _ => s"#/components/schemas/${nominal.fullyQualified.replace(".", "_")}}"
}
}
Expand Down

0 comments on commit 036f03f

Please sign in to comment.