Hi all,
I’m trying to add JSON schema to our API. I found an issue using enums as discriminators. The below example generates a schema for the enum like this:
"Shape": {
"title": "Shape",
"type": "string",
"enum": [
"oval",
"angular"
],
"const": "oval"
},
It contains enum
and const
which cause an issue when trying to validate the schema as a validator treats it as const. Probably generating the discriminator as an enum is wrong. It should be a string instead of a reference to the enum. The workaround which I did, is to pass the Schema.string[Shape]
as a schema for discriminator. I’m not sure if that is a bug or jsut wrong usage.
Full example:
//> using scala "2.13.16"
//> using dependency "com.beachape::enumeratum-circe::1.7.5"
//> using dependency "com.beachape::enumeratum::1.7.5"
//> using dependency "com.softwaremill.sttp.apispec::jsonschema-circe::0.11.7"
//> using dependency "com.softwaremill.sttp.tapir::tapir-enumeratum::1.11.15"
//> using dependency "com.softwaremill.sttp.tapir::tapir-json-circe::1.11.15"
//> using dependency "com.softwaremill.sttp.tapir::tapir-openapi-docs::1.11.15"
//> using dependency "io.circe::circe-generic-extras::0.14.4"
//> using dependency "io.circe::circe-generic::0.14.10"
//> using dependency "org.typelevel::cats-effect::3.5.7"
import sttp.tapir.SchemaType._
import sttp.tapir.Schema.SName
import enumeratum.EnumEntry.Snakecase
import enumeratum._
import cats.effect.{IO, IOApp}
import sttp.tapir.codec.enumeratum.TapirCodecEnumeratum
import sttp.tapir.{Codec => _, _}
import sttp.tapir.docs.apispec.schema._
import sttp.apispec.circe._
import io.circe.Json
import io.circe.syntax._
import io.circe.generic.extras.Configuration
import io.circe.generic.extras.semiauto._
import io.circe.generic.extras.auto._
import io.circe._
sealed trait Shape extends Snakecase
object Shape
extends Enum[Shape]
with CirceEnum[Shape]
with TapirCodecEnumeratum {
case object Oval extends Shape
case object Angular extends Shape
val values = findValues
implicit val schema: Schema[Shape] = schemaForEnumEntry[Shape]
}
sealed trait Figure
object Figure {
implicit val config: Configuration = Configuration.default
implicit val schema: Schema[Figure] = {
val derived = Schema.derived[Figure]
val mapping = Map(
Shape.Oval.entryName -> SRef(SName("<empty>.Circle")),
Shape.Angular.entryName -> SRef(SName("<empty>.Rectangle"))
)
derived.schemaType match {
case s: SchemaType.SCoproduct[Figure] =>
derived.copy(schemaType =
s.addDiscriminatorField(FieldName("shape"), Shape.schema, mapping)
)
case _ => derived
}
}
implicit val codec: Codec[Figure] = Codec.from[Figure](
Decoder.instance { cursor =>
cursor.downField("shape").as[Shape].flatMap {
case Shape.Oval => cursor.as[Circle]
case Shape.Angular => cursor.as[Rectangle]
}
},
Encoder.instance {
case tree: Circle =>
tree.asJson.mapObject(_.add("shape", (Shape.Oval: Shape).asJson))
case bush: Rectangle =>
bush.asJson.mapObject(_.add("shape", (Shape.Angular: Shape).asJson))
}
)
}
final case class Circle(radius: Int) extends Figure
final case class Rectangle(height: Int, width: Int) extends Figure
object Main extends IOApp.Simple {
def run = IO.println(
TapirSchemaToJsonSchema(
Figure.schema,
markOptionsAsNullable = true
).asJson.spaces2
)
}