softwaremill / tapir

Rapid development of self-documenting APIs

Home Page:https://tapir.softwaremill.com

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

OpenAPI schema for union types is missing the discriminator level.

kamilkloch opened this issue · comments

Repo with code: https://github.com/kamilkloch/tapir-union-types-schema

sealed trait Fruit

object Fruit {
  case class Apple(color: String) extends Fruit

  case class Potato(weight: Double) extends Fruit

  private implicit val config: Configuration = Configuration.default
  implicit val fruitCodec: io.circe.Codec[Fruit] = deriveConfiguredCodec
}

Generated OpenApi YAML is incorrect - it is missing the discriminant level. An Apple (from Fruit trait perspective) is of shape

{ 
  "Apple" : { 
    "color": "string"
  }
}

, and not

{ 
  "color": "string"
}

Generated OpenApi YAML:

openapi: 3.1.0
info:
  title: linguistic-nightingale
  version: 1.0.0
paths:
  /fruit:
    post:
      operationId: postFruit
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Fruit'
        required: true
      responses:
        '200':
          description: ''
        '400':
          description: 'Invalid value for: body'
          content:
            text/plain:
              schema:
                type: string
components:
  schemas:
    Apple:
      required:
      - color
      type: object
      properties:
        color:
          type: string
    Fruit:
      oneOf:
      - $ref: '#/components/schemas/Apple'
      - $ref: '#/components/schemas/Potato'
    Potato:
      required:
      - weight
      type: object
      properties:
        weight:
          type: number
          format: double

As I understand the problem is that there's a mismatch between the generated schema, and the way an object is serialised to JSON. As tapir doesn't know what's your JSON library of configuration, you have to configure it separately (unless you're using pickler).

So in this case, you'll have to provide the schemas by hand, see: https://tapir.softwaremill.com/en/latest/endpoint/schemas.html#wrapper-object-discriminators

Thank you for pointing to https://tapir.softwaremill.com/en/latest/endpoint/schemas.html#wrapper-object-discriminators.

Schema.oneOfWrapped takes an implicit sttp.tapir.generic.Configuration, but (understandably) ignores its .withDiscriminator setting. On the other hand, Schema.derivedSchema does not ignore the .withDiscriminator setting. This means, that the schema configuration logic is split in 2 places: sttp.tapir.generic.Configuration and invocation of either Schema.oneOfWrapped or Schema.derivedSchema.

It looks like the most 2 common semi-automatic schema types (and Json codecs) for coproducts are

  1. wrapper object discriminators, or
  2. field dicriminators.

Arguably, the default tapir schema (no wrapper object discriminators, no field discriminators) does not reflect the default for most common codecs (circe and jsoniter-scala for sure).

Proposition: Schema.oneOfWrapped behavior could be the default behavior for union types (like in circe), changeable to discriminator fields via .withDiscriminator setting in the Configuration. Schema.oneOfWrapped method could be removed altogether, Schema.derivedSchema could be used instead, tapping into the single source of truth via implicit Configuration.

I'm afraid we can't really change the default strategy for schemas generation for coproducts - even though binary compatible, it would be a breaking change in the core module. Although, I agree that the current situation is not perfect - but I think we'll have to resort to having this properly documented, at least in the 1.x versions.

The situation is different with pickler, where we are free to define the configuration and serialization. That's still very much in the works (cc @kciesielski), so maybe there we can improve basing on your input.

(btw. the defaults on how coproducts are serialised are different for each json library, so it's hard to pick a good candidate ;) but that also would be fixed by pickler, which derives both json codecs & schemas in one go)