-
Notifications
You must be signed in to change notification settings - Fork 422
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
Alignment of tapir and circe with respect of coproduct support with discriminator #315
Comments
For clarity I wrote code to produce a valid openapi spec and use the circe codec: sealed trait TestType
case class CaseClass(a: String) extends TestType
case object CaseObject extends TestType
object TestType {
private[this] val typeDiscriminator = "typeDiscrimininator"
private[this] implicit val configuration: Configuration =
Configuration.default.withDiscriminator(typeDiscriminator)
implicit val codec: Codec.AsObject[TestType] =
io.circe.generic.extras.semiauto.deriveConfiguredCodec[TestType]
private[this] val caseClassSchema: Schema[CaseClass] = {
val tempSchema = implicitly[Schema[CaseClass]]
val schemaType = tempSchema.schemaType.asInstanceOf[SProduct]
tempSchema.copy(
schemaType = schemaType.copy(fields = (typeDiscriminator -> Schema(
SString,
isOptional = false) :: schemaType.fields.toList)))
}
private[this] val caseObjectSchema: Schema[CaseObject.type] = {
val tempSchema = implicitly[Schema[CaseObject.type]]
val schemaType = tempSchema.schemaType.asInstanceOf[SProduct]
tempSchema.copy(
schemaType = schemaType.copy(fields = (typeDiscriminator -> Schema(
SString,
isOptional = false) :: schemaType.fields.toList)))
}
implicit val conditionSchema: Schema[TestType] = {
val tempSchema = Schema
.oneOf[TestType, String](_.toString, identity)(
"CaseClass" -> caseClassSchema,
"CaseObject" -> caseObjectSchema)
val schemaType = tempSchema.schemaType.asInstanceOf[SCoproduct]
tempSchema.copy(
schemaType = schemaType.copy(
discriminator = schemaType.discriminator.map(discriminator => {
discriminator.copy(propertyName = typeDiscriminator)
})))
} this produces: components:
schemas:
TestType:
oneOf:
- $ref: '#/components/schemas/CaseClass'
- $ref: '#/components/schemas/CaseObject'
discriminator:
propertyName: typeDiscrimininator
mapping:
CaseClass: '#/components/schemas/CaseClass'
CaseObject: '#/components/schemas/CaseObject'
CaseClass:
required:
- typeDiscrimininator
- a
type: object
properties:
typeDiscrimininator:
type: string
a:
type: string
CaseObject:
required:
- typeDiscrimininator
type: object
properties:
typeDiscrimininator:
type: string I know that the code could be simplified by reducing redundant code and using lenses but I think it would be great to get this into tapir somehow :) I would appreciate input on how it could be achieved. If we can agree on a possible solution I would be willing to invest some to help implementing it, however currently I don't see the best path towards it. |
To have a full typesafety it would be nice to also have |
Any progress on this? |
I ran into this exact problem and ended up writing a function to auto-generate coproduct schemas with discriminator keys. This will also inject the discriminator field into each coproduct schema, that's optional and can be removed. It's there because, for example, Loopback's validation against OpenAPI requires that the field exists. I'm posting it here in case it helps someone else finding their way to this issue, like I did previously. // Usage example
import my.package.TapirCoproductSchema.deriveCoproductSchema
object MyTrait {
implicit val schema: Schema[MyTrait] = deriveCoproductSchema[MyTrait]()
}
sealed trait MyTrait
case object Foo extends MyTrait
case class Bar(x: Int) extends MyTrait package my.package
import scala.annotation.nowarn
import scala.reflect.runtime.universe.WeakTypeTag
import shapeless.{Coproduct, Generic, HList, LabelledGeneric, ops}
import sttp.tapir.{FieldName, Schema}
import sttp.tapir.SchemaType.{Discriminator, SCoproduct, SObject, SObjectInfo, SProduct, SRef, SString}
class TapirCoproductSchema[A] {
val discriminatorKey = "kind"
def apply[C <: Coproduct, C0 <: Coproduct, L <: HList, K <: HList]()(implicit
// FIXME: make sure the ordering of gen and lgen is the same
@nowarn gen: Generic.Aux[A, C],
// Used to get all the symbol names
@nowarn lgen: LabelledGeneric.Aux[A, C0],
// Lift all schemas for the coproducts
schemas: ops.coproduct.LiftAll.Aux[Schema, C, L],
schemasToSeq: ops.hlist.ToTraversable.Aux[L, Seq, Schema[_]],
// Get the keys for the coproducts
keys: ops.union.Keys.Aux[C0, K],
keysToSeq: ops.hlist.ToTraversable.Aux[K, Seq, Symbol],
typeTag: WeakTypeTag[A]
): Schema[A] = {
val typeName = typeTag.tpe.typeSymbol.fullName
// "CoproductName" -> Schema[CoproductName]
val mapping = keysToSeq(keys()).map(_.name).zip(schemasToSeq(schemas.instances)).map {
// Add discriminator key to all product schemas
case (key, schema @ Schema(schemaType: SProduct, _, _, _, _)) =>
key -> schema.copy(
schemaType = schemaType.copy(
fields = schemaType.fields.toSeq :+ (FieldName(discriminatorKey) -> Schema(SString))
)
)
// Convert object schemas to product schemas containing only the discriminator key
case (key, schema @ Schema(schemaType: SObject, _, _, _, _)) =>
key -> schema.copy(
schemaType = SProduct(schemaType.info, Seq(FieldName(discriminatorKey) -> Schema(SString)))
)
case (key, _) =>
throw new Exception(s"Expected an object or product schema for $key in $typeName")
}.toMap
val discriminator = Discriminator(discriminatorKey, mapping.map {
case (k, sf) =>
// asInstanceOf will never fail as an exception would be thrown above first
k -> SRef(sf.schemaType.asInstanceOf[SObject].info)
})
Schema(
SCoproduct(
SObjectInfo(
typeName,
Nil
),
mapping.values.toList,
Some(discriminator)
)
)
}
}
object TapirCoproductSchema {
def deriveCoproductSchema[A] = new TapirCoproductSchema[A]
}
|
I've added a Let me know if that would solve the problems described above. Also, the docs are changed, hopefully they are clear now, but if not - also let me know :) |
#315: configurable coproducts schema derivation
Fixed in 0.17.0-M4 |
Circe and tapir both support coproduct derivation for sealed trait types with a discriminator field.
However currently the features are not very good aligned to work together.
Issue is a result of a gitter discussion with @ghostbuster91
Circe coproduct derivation:
https://circe.github.io/circe/codecs/adt.html#the-future
circe allows for automatical derivation of a sealed trait with a configurable field as dicriminator
tapir coproduct support
https://tapir-scala.readthedocs.io/en/latest/endpoint/customtypes.html#sealed-traits-coproducts
tapir supports something similar by deriving Schema for TypeA and then using Schema.oneOf to define a Schema for SealedTrait.
However this only generates a valid openapi-spec if the defined discriminator field is also a field of TypeA like
case class TypeA(i: Int, what_am_i: String = "TypeA")
as the autogenerated Schema for TypeA only then contains the field "what_am_i" in the OpenApi Spec. I believe this is not a satisfactory solution as the field should not be changeable and also the autogenerated circe codec has redundant fields as it has a Configuration for the discriminating field but also sees the field in the case classes.I believe the Documentation is misleading as the example code does not lead to a valid openapi spec. More serious the only clean solution for this in my opinion is modifying the autogenerated Schema for TypeA and add the "what_am_i" field.
suggested solution
I would like a possibility to get a Schema and a corresponding circe Codec with discriminator for a sealed trait that is easy to generate and in the best case guarantees that what is in the openapi-spec is also what circe understands.
Maybe the oneOf makro could alter the autogenerated Schema of the case classes?
In this case the oneOf code would also not need an extractor as the field does not really exist in the case classes when using the circe autoderivation as stated above. It could accept a circe Configuration object or something similar.
The text was updated successfully, but these errors were encountered: