tapir
tapir copied to clipboard
[BUG] Schema derivation for parametrized data
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
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.
For a follow-up discussion on implementing this properly in Scala 3, see: https://github.com/lampepfl/dotty/discussions/14291