So I’m trying a weird hack (doesn’t matter why, assume some legacy tech debt that binds my hands).
Consider the following wrapper:
import zio.json.{ jsonField, JsonDecoder, JsonEncoder }
import zio.json.ast.Json
case class WithExtras[+T <: HugeLegacySumType](
datatype: T,
@jsonField(WithExtras.extrasField) extras: Option[Json]
)
object WithExtras {
val extrasField: String = "extras"
implicit def jsonEncoder[T <: HugeLegacySumType: JsonEncoder]: JsonEncoder[WithExtras[T]] = {
val encoder = implicitly[JsonEncoder[T]]
Json.Obj.encoder.contramap[WithExtras[T]] { case WithExtras(wrapped, extras) =>
encoder.toJsonAST(wrapped) match {
case Left(encodingError) => throw new RuntimeException("Unreachable code path[encode!]: " + encodingError)
case Right(jo: Json.Obj) => extras.fold(jo)(jo.add(extrasField, _))
case Right(anyOtherJson) => throw new RuntimeException("Unreachable code path[non-obj]: " + anyOtherJson.toJson)
}
}
}
implicit def jsonDecoder[T <: HugeLegacySumType: JsonDecoder]: JsonDecoder[WithExtras[T]] = {
val decoder = implicitly[JsonDecoder[T]]
Json.Obj.decoder.mapOrFail[WithExtras[T]] { obj =>
decoder.fromJsonAST(obj).map(WithExtras(_, obj.get(extrasField)))
}
}
}
TL;DR:
I can’t change the API, but need to add a custom free “extras” JSON field.
The custom encoder/decoder just adds this optional field to the object.
disregard the fact that this is not “safe” (all subtypes of HugeLegacySumType are case classes, and thus serialized as JSON objects, and no subtype contains a field named “extras”).
How would I provide tapir Schema[WithExtras[HugeLegacySumType]] that also adds the optional “extras” field to each subtype of HugeLegacySumType?
Thanks @adamw, I feared that might be the case.
Is there a way to somehow reuse automatic schema derivation for HugeLegacySumType?
It would not make sense to go full manual.
perhaps something like:
val schema = Schema.derive[HugeLegacySumType]
val schemaType = schema.schemaType match {
case SCoproduct(allSubtypes) => ???
case _ => throw new RuntimeException("unreachable code path")
}
schema.copy(schemaType = schemaType)
I think this depends on … if you want to add the “extras” field to each member of the coproduct? Then, you’d need to transform the schema dervied for HugeLegacySumType and replace each SchemaType.SProduce with SchemaType.SOpenProduct. I think this could be done generically.
Or, do you want to create a schema representing a wrapper type, where you have an “extras” field and a “data” field?
sealed trait Test
case class X(a: Int) extends Test
case class Y(b: String, c: Double) extends Test
and the schema translation:
val s = Schema.derived[Test]
val s2 = s.copy(schemaType = s.schemaType match {
case co@SchemaType.SCoproduct(subtypes, discriminator) =>
val subtypes2 = subtypes.map {
case s@Schema(SchemaType.SProduct(fields), _, _, _, _, _, _, _, _, _, _) =>
// we're not doing any validation on the "extra" fields, hence using Map.empty as the value of the extracted fields
s.copy(schemaType = SchemaType.SOpenProduct(fields, Schema.anyObject)(_ => Map.empty))
case s => s // unchanged
}
SchemaType.SCoproduct(subtypes2, discriminator)(co.subtypeSchema)
case _ => throw new IllegalStateException("Expected SCoproduct")
})
if you then use s2 as the schema for your value, you’ll get the following in openapi: