Json schema - wrong schema for enum

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
  )
}

It was a bug :slight_smile: Fix OpenAPI schema generation when the discriminator field is an enum by adamw · Pull Request #4379 · softwaremill/tapir · GitHub

1 Like