I am trying to define an avro4s schema derivation for a common trait. Example
trait Event
case class UserCreated(name: String, age: Int) extends Event
case class UserDeleted(reason: String) extends Event
Derivation for the concrete classes seems straightforward:
given userCreatedSchemaFor = SchemaFor[UserCreated]
given userDeletedSchemaFor = SchemaFor[UserDeleted]
I would love to achieve something like this fake code:
given eventSchemaFor = new SchemaFor[Event] {
CASE (userCreated) => USE userCreatedSchemaFor
CASE (userDeleted) => USE userDeletedSchemaFor
}
The SchemaFor trait & object expose some apply variants and 'factory' methods but I can't figure out a way to use them to my purpose. Thanks for any help.
The answer by @stefanobaghino is correct if you can seal the trait, but in my case I was interested in an unsealed trait. The solution to generate a schema was quite near - avro4s provides a Schema.createUnion
which is great for this usecase.
The next problem you might have is encoding for serialization (after all we want to do something useful with this data). For that, the best way I found was the manual creation of an Encoder. Follows a scala-cli script that shows everything put together.
//> using scala 3.3.1
//> using dep com.sksamuel.avro4s::avro4s-core::5.0.9
import org.apache.avro.Schema
import com.sksamuel.avro4s.{AvroOutputStream, AvroSchema, Encoder, SchemaFor}
import java.io.ByteArrayOutputStream
import scala.util.Using
trait Event
case class UserCreated(age: Int) extends Event
case class UserDeleted(reason: String) extends Event
case class Envelope(name: String, event: Event) // <- here I use the trait
// individual schemas
given userCreatedSchema: Schema = AvroSchema[UserCreated]
given userDeletedSchema: Schema = AvroSchema[UserDeleted]
given unionSchema: Schema = Schema.createUnion(
AvroSchema[UserCreated],
AvroSchema[UserDeleted],
)
given unionSchemaFor: SchemaFor[Event] = SchemaFor.from[Event](unionSchema)
val userCreated = UserCreated(18)
val userDeleted = UserDeleted("boring")
val envelopeUserCreated = Envelope("uc", userCreated)
val envelopeUserDeleted = Envelope("ud", userDeleted)
given encoderEvent: Encoder[Event] = (schema: Schema) => {
case e: UserCreated =>
summon[Encoder[UserCreated]].encode(userCreatedSchema)(e)
case e: UserDeleted =>
summon[Encoder[UserDeleted]].encode(userDeletedSchema)(e)
case _ => throw new Exception()
}
object Main extends App:
val byteStream = new ByteArrayOutputStream
val jsonOutcome = Using(AvroOutputStream.json[Envelope].to(byteStream).build()) { aos =>
aos.write(envelopeUserCreated)
aos.write(envelopeUserDeleted)
aos.flush()
byteStream.toString
}
println(jsonOutcome)