Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUG] Recursive ADT's Schemas can't be implicit for auto derivation #945

Closed
DenisNovac opened this issue Jan 17, 2021 · 2 comments
Closed

Comments

@DenisNovac
Copy link
Contributor

DenisNovac commented Jan 17, 2021

Tapir version: 0.17.2

Scala version: 2.13.4

Describe the bug

When you have some ADT with recursive types - automatic schema derivation will fail in runtime if your schema is implicit:

tapir-test[ERROR] Exception in thread "main" java.lang.ExceptionInInitializerError
tapir-test[ERROR]       at Endpoints.<init>(Endpoints.scala:17)
tapir-test[ERROR]       at Main$.<clinit>(Main.scala:13)
tapir-test[ERROR]       at Main.main(Main.scala)
tapir-test[ERROR] Caused by: java.lang.NullPointerException
tapir-test[ERROR]       at sttp.tapir.Schema$.schemaForIterable(Schema.scala:140)
tapir-test[ERROR]       at Animal$.$anonfun$schema$1(Animal.scala:22)
tapir-test[ERROR]       at magnolia.CallByNeed.value$lzycompute(magnolia.scala:818)
tapir-test[ERROR]       at magnolia.CallByNeed.value(magnolia.scala:817)
tapir-test[ERROR]       at magnolia.Subtype$$anon$1.typeclass(interface.scala:73)
tapir-test[ERROR]       at sttp.tapir.generic.internal.SchemaMagnoliaDerivation.$anonfun$dispatch$1(SchemaMagnoliaDerivation.scala:101)
tapir-test[ERROR]       at scala.collection.immutable.ArraySeq.$anonfun$map$1(ArraySeq.scala:71)
tapir-test[ERROR]       at scala.collection.immutable.ArraySeq.$anonfun$map$1$adapted(ArraySeq.scala:71)
tapir-test[ERROR]       at scala.collection.immutable.ArraySeq$.tabulate(ArraySeq.scala:286)
tapir-test[ERROR]       at scala.collection.immutable.ArraySeq$.tabulate(ArraySeq.scala:265)
tapir-test[ERROR]       at scala.collection.ClassTagIterableFactory$AnyIterableDelegate.tabulate(Factory.scala:679)
tapir-test[ERROR]       at scala.collection.immutable.ArraySeq.map(ArraySeq.scala:71)
tapir-test[ERROR]       at scala.collection.immutable.ArraySeq.map(ArraySeq.scala:35)
tapir-test[ERROR]       at sttp.tapir.generic.internal.SchemaMagnoliaDerivation.dispatch(SchemaMagnoliaDerivation.scala:101)
tapir-test[ERROR]       at sttp.tapir.generic.internal.SchemaMagnoliaDerivation.dispatch$(SchemaMagnoliaDerivation.scala:100)
tapir-test[ERROR]       at sttp.tapir.Schema$.dispatch(Schema.scala:99)
tapir-test[ERROR]       at Animal$.<clinit>(Animal.scala:22)

How to reproduce?

Example app is here: https://github.com/DenisNovac/tapir-schema-test

Model Animal (https://github.com/DenisNovac/tapir-schema-test/blob/master/src/main/scala/Animal.scala):

sealed trait Animal {
  def name: String
}

object Animal extends AutoDerivation with SchemaDerivation {

  implicit val customConfig: CirceConfiguration =
    CirceConfiguration.default.withDefaults.withDiscriminator("type")

  implicit val codec: Codec[Animal] = deriveConfiguredCodec

  implicit val tapirConfig: TapirConfiguration = TapirConfiguration.default.withDiscriminator("type")

  // It can't be implicit since recursive derivation fails
  implicit val schema: Schema[Animal] = Schema.derived

  case class Dog(name: String, training: Boolean, friends: List[Animal]) extends Animal
  case class Cat(name: String, favToy: String, friends: List[Animal])    extends Animal
}

When schema is not implicit - it won't be used. Tapir won't show the discriminator:

Dog:
      required:
      - name
      - training
      type: object
      properties:
        name:
          type: string
        training:
          type: boolean
        friends:
          type: array
          items:
            $ref: '#/components/schemas/Animal'

But you can still run your app since Animal extends the SchemaDerivation trait. But this trait does not use configuration so discriminator does not appears.

The workaround is to import the Animal.schema to your routes description (https://github.com/DenisNovac/tapir-schema-test/blob/master/src/main/scala/Endpoints.scala) and make it implicit there:

class Endpoints {

  /* You won't see Tapir's discriminator without this. But json with "type" will work fine since it uses Circe's
   * discriminator while decoding */
  implicit val animalS: Schema[Animal] = Animal.schema

  private val baseEndpoint: Endpoint[Unit, Unit, Unit, Any] =
    endpoint
      .in("api" / "v1")

  val postAnimal: Endpoint[Animal, Unit, Animal, Any] =
    baseEndpoint.post
      .in("animal")
      .in(jsonBody[Animal])
      .out(jsonBody[Animal])

}

This way you'll see the discriminator:

    Dog:
      required:
      - name
      - training
      - type
      type: object
      properties:
        name:
          type: string
        training:
          type: boolean
        friends:
          type: array
          items:
            $ref: '#/components/schemas/Animal'
        type:
          type: string

Every time you open your Swagger and see no discriminator somewhere you need to go to this type, remove SchemaDerivation and see where else you need to import Animal.schema explicitly.

@adamw
Copy link
Member

adamw commented Jan 18, 2021

For recursive schemas you'll need a lazy val - I've added a note about that to the docs. Also, I've improved caching of recursive coproduct (sealed traits) schemas - see the PR.

@adamw adamw closed this as completed in 8891c70 Jan 18, 2021
adamw added a commit that referenced this issue Jan 18, 2021
Fixes #945: also cache coproducts during derivation. Add note on semi-auto derivation of recursive schemas.
@adamw
Copy link
Member

adamw commented Jan 18, 2021

Released in 0.17.5

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants