tapir icon indicating copy to clipboard operation
tapir copied to clipboard

[BUG] Schema derivation for parametrized data

Open oskin1 opened this issue 4 years ago • 2 comments

Tapir version: 0.18.0-M4

Scala version: 2.12.13

Describe the bug

Schema derived for parametrized case classes is always fixed to a random parameter

How to reproduce?

final case class Items[A](items: List[A], total: Int)
object Items {
  implicit def schema[A: Schema]: Schema[Items[A]]    = Schema.derived[Items[A]]
  implicit def validator[A: Schema]: Validator[Items[A]] = schema.validator
}

Results in the following openapi spec:

    Items_A:
      required:
      - total
      type: object
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/TransactionInfo'
        total:
          type: integer

And then Items_A is referenced not only where Items[TransactionInfo] is used but everywhere Items[X] appears.

Additional information

oskin1 avatar Jun 09 '21 17:06 oskin1

This is an interesting problem, which is another way of saying - I'm not sure how to fix this :)

The problem is that when doing:

implicit def schema[A: Schema]: Schema[Items[A]] = Schema.derived[Items[A]]

the macro (here Schema.derived) is called when compiling the schema method. At that point, the precise type of A is unknown (as it can be anything). The resulting schema uses the implicitly passed Schema[A] method. Hence, the result is:

Schema(SProduct(List(SProductField(FieldName(items,items),Schema(SArray(Schema(SInteger(),None,false,None,None,None,None,false,All(List()))),None,true,None,None,None,None,false,All(List()))))),Some(SName(sttp.tapir.test.Test.Items,List(A))),false,None,None,None,None,false,All(List()))

where the name of the schema contains the abstract A: SName(sttp.tapir.test.Test.Items,List(A)), but the specific instance contains the correct element schema (here an SInteger). When generating documentation, schemas are keyed by name, so a random one is chosen.

Maybe surprisingly, this is fixed by using auto-derivation (import sttp.tapir.generic.auto._), as then the macro call is done at the final usage-site, so full information on type parameters is available. So we get the downside of automatic schema generation - that the macro is called multiple times - but at least it has the correct type parameters.

One solution requires Scala 3, inlining the implicit solves the problem:

implicit def schema[A: Schema]: Schema[Items[A]] = macro Schema.derivedMacro[Items[A]]

but then again, having case class Items[A](items: List[A]) derives Schema again breaks things, as the auto-generated given isn't inlined.

So other than documenting this and either suggesting auto-derivation, or defining the implicit for concrete type parameters, I can't come up with a good solution.

adamw avatar Jan 18 '22 10:01 adamw

For a follow-up discussion on implementing this properly in Scala 3, see: https://github.com/lampepfl/dotty/discussions/14291

adamw avatar Jan 18 '22 11:01 adamw