softwaremill / tapir

Rapid development of self-documenting APIs
https://tapir.softwaremill.com
Apache License 2.0
1.36k stars 418 forks source link

[BUG] Validator reference is already set to Product #946

Closed DenisNovac closed 3 years ago

DenisNovac commented 3 years ago

Tapir version: 0.17.2

Scala version: 2.13.4

Describe the bug

Some strange parts of the code fails with error:

tapir-test[ERROR] Exception in thread "main" java.lang.ExceptionInInitializerError
tapir-test[ERROR]       at Endpoints.<init>(Endpoints.scala:8)
tapir-test[ERROR]       at Main$.<clinit>(Main.scala:13)
tapir-test[ERROR]       at Main.main(Main.scala)
tapir-test[ERROR] Caused by: java.lang.IllegalArgumentException: Validator reference is already set to Product(Map(friends -> sttp.tapir.generic.internal.SchemaMagnoliaDerivation$$anon$1@6e2c9341))!
tapir-test[ERROR]       at sttp.tapir.Validator$Ref.set(Validator.scala:228)
tapir-test[ERROR]       at sttp.tapir.generic.internal.SchemaMagnoliaDerivation.$anonfun$combine$2(SchemaMagnoliaDerivation.scala:33)
tapir-test[ERROR]       at scala.Option.foreach(Option.scala:437)
tapir-test[ERROR]       at sttp.tapir.generic.internal.SchemaMagnoliaDerivation.$anonfun$combine$1(SchemaMagnoliaDerivation.scala:33)
tapir-test[ERROR]       at sttp.tapir.generic.internal.SchemaMagnoliaDerivation.withProgressCache(SchemaMagnoliaDerivation.scala:76)
tapir-test[ERROR]       at sttp.tapir.generic.internal.SchemaMagnoliaDerivation.combine(SchemaMagnoliaDerivation.scala:16)
tapir-test[ERROR]       at sttp.tapir.generic.internal.SchemaMagnoliaDerivation.combine$(SchemaMagnoliaDerivation.scala:15)
tapir-test[ERROR]       at Animal$.combine(Animal.scala:17)
tapir-test[ERROR]       at Animal$.wildcatTypeclass$macro$117$lzycompute$1(Animal.scala:27)
tapir-test[ERROR]       at Animal$.wildcatTypeclass$macro$117$1(Animal.scala:27)
tapir-test[ERROR]       at Animal$.$anonfun$schema$12(Animal.scala:27)
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 Animal$.dispatch(Animal.scala:17)
tapir-test[ERROR]       at Animal$.petTypeclass$macro$108$lzycompute$1(Animal.scala:27)
tapir-test[ERROR]       at Animal$.petTypeclass$macro$108$1(Animal.scala:27)
tapir-test[ERROR]       at Animal$.paramTypeclass$macro$107$lzycompute$1(Animal.scala:27)
tapir-test[ERROR]       at Animal$.paramTypeclass$macro$107$1(Animal.scala:27)
tapir-test[ERROR]       at Animal$.$anonfun$schema$20(Animal.scala:27)
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.ReadOnlyParam$$anon$2.typeclass(interface.scala:173)
tapir-test[ERROR]       at sttp.tapir.generic.internal.SchemaMagnoliaDerivation.$anonfun$productValidator$1(SchemaMagnoliaDerivation.scala:112)
tapir-test[ERROR]       at scala.collection.immutable.List.flatMap(List.scala:293)
tapir-test[ERROR]       at sttp.tapir.generic.internal.SchemaMagnoliaDerivation.productValidator(SchemaMagnoliaDerivation.scala:111)
tapir-test[ERROR]       at sttp.tapir.generic.internal.SchemaMagnoliaDerivation.$anonfun$combine$1(SchemaMagnoliaDerivation.scala:25)
tapir-test[ERROR]       at sttp.tapir.generic.internal.SchemaMagnoliaDerivation.withProgressCache(SchemaMagnoliaDerivation.scala:76)
tapir-test[ERROR]       at sttp.tapir.generic.internal.SchemaMagnoliaDerivation.combine(SchemaMagnoliaDerivation.scala:16)
tapir-test[ERROR]       at sttp.tapir.generic.internal.SchemaMagnoliaDerivation.combine$(SchemaMagnoliaDerivation.scala:15)
tapir-test[ERROR]       at sttp.tapir.Schema$.combine(Schema.scala:99)
tapir-test[ERROR]       at Animal$.catTypeclass$macro$104$lzycompute$1(Animal.scala:27)
tapir-test[ERROR]       at Animal$.catTypeclass$macro$104$1(Animal.scala:27)
tapir-test[ERROR]       at Animal$.$anonfun$schema$1(Animal.scala:27)
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$.animalTypeclass$macro$101$lzycompute$1(Animal.scala:27)
tapir-test[ERROR]       at Animal$.animalTypeclass$macro$101$1(Animal.scala:27)
tapir-test[ERROR]       at Animal$.<clinit>(Animal.scala:27)

How to reproduce?

Project with example (specific branch in repo for it): https://github.com/DenisNovac/tapir-schema-test/tree/multilevel-traits-adt

Animal (https://github.com/DenisNovac/tapir-schema-test/blob/multilevel-traits-adt/src/main/scala/Animal.scala):

sealed trait Animal {
  def name: String
}

sealed trait Pet extends Animal {
  def name: String
  def favToy: 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
  val schema: Schema[Animal] = Schema.derived

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

  /* Works */

  //case class WildCat(name: String, friends: List[Animal])                             extends Animal

  /* Fails */

  // this is copy of Cat class but still fails
  //case class WildCat(name: String, favToy: String, friends: List[Pet]) extends Pet

  // this one is the most useful i guess
  case class WildCat(name: String, favToy: String, friends: List[Animal]) extends Pet

  //case class WildCat(name: String, friends: List[Pet]) extends Animal
  //case class WildCat(name: String, favToy: String, street: String, friends: List[Pet]) extends Pet
  //case class WildCat(name: String, favToy: String, friends: List[Pet], street: String) extends Pet

}
DenisNovac commented 3 years ago

Actually it happens even without multiple traits:

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
  val schema: Schema[Animal] = Schema.derived

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

  //Fails:

  //case class Mouse(name: String, favToy: String, friends: List[Animal])   extends Animal
  //case class Mouse(name: String, favToy: String, friends: List[Animal], flag: Boolean)   extends Animal

}
DenisNovac commented 3 years ago

Somehow it even allows to have two same case classes but fails on third.

final case class Tiger(name: String)                        extends Animal
final case class Elephant(name: String)                     extends Animal
final case class Dog(name: String, friends: List[Animal])   extends Animal
final case class Cat(name: String, friends: List[Animal])   extends Animal
final case class Mouse(name: String, friends: List[Animal]) extends Animal // it fails only with this line
adamw commented 3 years ago

The derivation cache wasn't cleared at one point. A fix is coming :)

DenisNovac commented 3 years ago

@adamw that's great, thank you

adamw commented 3 years ago

Released in 0.17.5