jsonscalacircespray-json

How to serialize and deserialize traits, to and from Json, in Scala?


First Attempt:

So far I have tried spray-json. I have:


trait Base

case class A ( id: String) extends Base

case class B (id: String) extends Base

Now, for serializing and deserializing my Base type, I have the code:

implicit object BaseFormat extends RootJsonFormat[Base]{
    def write(obj: Base): JsValue = {
      obj match {
        case a: A => a.toJson
        case b: B => b.toJson
        case unknown @ _ => serializationError(s"Marshalling issue with ${unknown}")
      }
    }

    def read(json: JsValue): Base = {
      //how to know whether json is encoding an A or a B?
    }
  }

The problem is that, for implementing the read method for deserialization, I can't figure out a way to know whether the JsValue is encoding an A or a B.

Second Attempt:

For solving this in spray-json, I ended up simply renaming the field id in A to aID, and in B to bID.

Third Attempt:

Since spray-json was not as sophisticated as the alternative libraries such as zio-json or circe, which handle this issue by themselves without additional code, I started using zio-json

Now I get the error

magnolia: could not infer DeriveJsonEncoder.Typeclass for type

for all the case classes taking type parameters. Also, it has problems with chained trait inheritance. It seems like circe uses magnolia, too. So it’s likely this would be replicated with circe, as well.

Any help would be appreciated.


Solution

  • You should address this problem using a json encoding/decoding library. Here is an example using circe and it's semi-automatic mode.

    Since you mentionned in the comments that you are struggling with generic types in your case classes, I'm also including that. Basically, to derive an encoder or decoder for a class Foo[T] that contains a T, you have to prove that there is a way to encode and decode T. This is done by asking for an implicit Encoder[T] and Decoder[T] where you derive Encoder[Foo[T]] and Decoder[Foo[T]]. You can generalize this reasoning to work with more than one generic type of course, you just have to have an encoder/decoder pair implicitly available where you derive the encoder/decoder for the corresponding case class.

    import io.circe._, io.circe.generic.semiauto._, io.circe.syntax._
    
    case class Foo[T](a: Int, b: String, t: T)
    object Foo {
      implicit def decoder[T: Encoder: Decoder]: Decoder[Foo[T]] = deriveDecoder
      implicit def encoder[T: Encoder: Decoder]: Encoder[Foo[T]] = deriveEncoder
    }
    
    case class Bar(a: Int)
    object Bar {
      implicit val encoder: Encoder[Bar] = deriveEncoder
      implicit val decoder: Decoder[Bar] = deriveDecoder
    }
    
    case class Baz(a: Int)
    
    println(Foo(42, "hello", 23.4).asJson) // Works because circe knows Encoder[Float] 
    println(Foo(42, "hello", Bar(42)).asJson) // Works because we defined Encoder[Bar] 
    
    //println(Foo(42, "hello", Baz(42)).asJson) // Doesn't compile: circe doesn't know Encoder[Baz] in semi-auto mode
    

    Try it live on Scastie

    Note that a different encoder/decoder for Foo[T] needs to be generated for each type T that you are using, which is why the encoder and decoder derivation for Foo have to be methods, not values like for Bar.

    There is also a fully-automatic mode, but it tends to produce compile-time errors that are harder to debug for beginner, so I would start with semi-auto. Another problem, is that auto-mode can take much longer to compile on large projects. If you're feeling adventurous, it's even more beautiful when you don't make mistakes!

    import io.circe._, io.circe.generic.auto._, io.circe.syntax._
    
    case class Foo[T](a: Int, b: String, t: T)
    case class Bar(a: Int)
    case class Baz(a: Int)
    
    println(Foo(42, "hello", 23.4).asJson) // circe knows Encoder[Float] and automatically derives Encoder[Foo[Float]]
    println(Foo(42, "hello", Bar(42)).asJson) // circe wants Encoder[Foo[Bar]], so it ma(cro)gically derives Encoder[Bar] and then Encoder[Foo[Bar]]
    println(Foo(42, "hello", Baz(42)).asJson) // Does compile this time, circe automatically find the encoder/decoder for Baz the same way it does for Bar
    

    Try it live on Scastie