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

1.5k Views Asked by At

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.

2

There are 2 best solutions below

7
francoisr On BEST ANSWER

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

0
zmerr On

A working example using traits with generics is this:

import io.circe.Decoder.Result
import io.circe._
import io.circe.generic.semiauto._
import io.circe.syntax._


trait MyBase[T <: MyBase[T]] {
  def myid: String
  def issue: MyBaseIssue
  def summary: MyBaseSummary[T]
}

trait MyBaseIssue

object MyBaseIssue {
  implicit val decodeIssue: Decoder[MyBaseIssue] =
    Decoder[SimpleBaseIssue].map[MyBaseIssue](identity).or(
      Decoder[SophisticatedBaseIssue].map[MyBaseIssue](identity)
    )

  implicit val encodeIssue: Encoder[MyBaseIssue] = Encoder.instance {
    case simple @ SimpleBaseIssue(_) => simple.asJson
    case sophisticated @ SophisticatedBaseIssue(_) => sophisticated.asJson

  }
}

trait MyBaseSummary[T <: MyBase[T]]

object MyBaseSummary {
  implicit def decodeSummary[T <: MyBase[T]: Encoder: Decoder]
  : Decoder[MyBaseSummary[T]] =
    Decoder[SimpleBaseSummary].map[MyBaseSummary[SimpleBase]](identity).asInstanceOf[Decoder[MyBaseSummary[T]]].or(
      Decoder[SophisticatedBaseSummary].map[MyBaseSummary[SophisticatedBase]](identity).asInstanceOf[Decoder[MyBaseSummary[T]]]
    )


  implicit def encodeSummary[T <: MyBase[T]: Encoder: Decoder]
  : Encoder[MyBaseSummary[T]] = Encoder.instance {
    case simple @ SimpleBaseSummary(_) => simple.asJson
    case sophisticated @ SophisticatedBaseSummary(_) => sophisticated.asJson

  }
}

case class SimpleBase(
                       myid: String,
                       override val issue: SimpleBaseIssue,
                       override val summary: SimpleBaseSummary
                     ) extends MyBase[SimpleBase]

case class SophisticatedBase(
                              myid: String,
                              extraField: String,
                              override val issue: SophisticatedBaseIssue,
                              override val summary: SophisticatedBaseSummary
                            ) extends MyBase[SophisticatedBase]

object SimpleBase {
  implicit val encoder: Encoder[SimpleBase] = deriveEncoder
  implicit val decoder: Decoder[SimpleBase] = deriveDecoder
}

object SophisticatedBase {
  implicit val encoder: Encoder[SophisticatedBase] = deriveEncoder
  implicit val decoder: Decoder[SophisticatedBase] = deriveDecoder
}

case class SimpleBaseIssue(simpleIssueType: String) extends MyBaseIssue
case class SophisticatedBaseIssue(sophisticatedIssueType: String) extends MyBaseIssue

object SimpleBaseIssue {
  implicit val encoder: Encoder[SimpleBaseIssue] = deriveEncoder
  implicit val decoder: Decoder[SimpleBaseIssue] = deriveDecoder
}

object SophisticatedBaseIssue {
  implicit val encoder: Encoder[SophisticatedBaseIssue] = deriveEncoder
  implicit val decoder: Decoder[SophisticatedBaseIssue] = deriveDecoder
}

case class SimpleBaseSummary(simpleSummary: String) extends MyBaseSummary[SimpleBase]

case class SophisticatedBaseSummary(sophisticatedSummary: String) extends MyBaseSummary[SophisticatedBase]

object SimpleBaseSummary {
  implicit val encoder: Encoder[SimpleBaseSummary] = deriveEncoder
  implicit val decoder: Decoder[SimpleBaseSummary] = deriveDecoder
}

object SophisticatedBaseSummary {
  implicit val encoder: Encoder[SophisticatedBaseSummary] = deriveEncoder
  implicit val decoder: Decoder[SophisticatedBaseSummary] = deriveDecoder
}

case class MyBaseList[T <: MyBase[T]](
                                       myid: String,
                                       var membersMap: Map[String, T] = Map.empty,
                                       override val issue: MyBaseListIssues[T],
                                       override val summary: MyBaseListSummary[T]
                                     ) extends MyBase[MyBaseList[T]]

object MyBaseList {

  implicit def membersMapDecoder[T <: MyBase[T]: Encoder: Decoder]
  : Decoder[Map[String, T]] = Decoder.decodeMap
  implicit def membersMapEncoder[T <: MyBase[T]: Encoder: Decoder]
  : Encoder[Map[String, T]] = Encoder.encodeMap


  implicit def encoder[T <: MyBase[T]: Encoder: Decoder]
  : Encoder[MyBaseList[T]] = deriveEncoder
  implicit def decoder[T <: MyBase[T]: Encoder: Decoder]
  : Decoder[MyBaseList[T]] = deriveDecoder
}

case class MyBaseListIssues[T <: MyBase[T]](
                                             issue: String,
                                             var set: Set[MyBaseIssue] = Set[MyBaseIssue]()
                                           ) extends MyBaseIssue

object MyBaseListIssues {
  implicit def membersMapDecoder: Decoder[Set[MyBaseIssue]] =
    Decoder.decodeSet[MyBaseIssue]
  implicit def membersMapEncoder: Encoder[Set[MyBaseIssue]] = Encoder.encodeSet

  implicit def encoder[T <: MyBase[T]: Encoder: Decoder]
  : Encoder[MyBaseListIssues[T]] = deriveEncoder
  implicit def decoder[T <: MyBase[T]: Encoder: Decoder]
  : Decoder[MyBaseListIssues[T]] = deriveDecoder
}

case class MyBaseListSummary[T <: MyBase[T]](
                                              summaryList: Map[String, MyBaseSummary[T]]
                                            ) extends MyBaseSummary[MyBaseList[T]]

object MyBaseListSummary {

  implicit def membersMapDecoder[T <: MyBase[T]: Encoder: Decoder]
  : Decoder[Map[String, MyBaseSummary[T]]] = Decoder.decodeMap
  implicit def membersMapEncoder[T <: MyBase[T]: Encoder: Decoder]
  : Encoder[Map[String, MyBaseSummary[T]]] = Encoder.encodeMap


  implicit def encoder[T <: MyBase[T]: Encoder: Decoder]
  : Encoder[MyBaseListSummary[T]] = deriveEncoder
  implicit def decoder[T <: MyBase[T]: Encoder: Decoder]
  : Decoder[MyBaseListSummary[T]] = deriveDecoder
}



object MainObject extends App {
  val simpleList = MyBaseList[SimpleBase]("simpleId",
    Map("one" -> SimpleBase("baseid", SimpleBaseIssue("the mistake"), SimpleBaseSummary("very concise"))),
    MyBaseListIssues[SimpleBase]("listIssue", Set(SimpleBaseIssue("a disaster"))),
    MyBaseListSummary[SimpleBase]( Map("simplebaseid" -> SimpleBaseSummary("super concise")))
  )
  val simpleJson = simpleList.asJson
  println(simpleJson)
  val convertedSimpleList = simpleJson.as[MyBaseList[SimpleBase]]
  println(convertedSimpleList)


  val sphisticatedList = MyBaseList[SophisticatedBase]("sophisticatedId",
    Map("one" -> SophisticatedBase("baseid", "further detail", SophisticatedBaseIssue("the mistake"), SophisticatedBaseSummary("very concise"))),
    MyBaseListIssues[SophisticatedBase]("listIssue", Set(SophisticatedBaseIssue("a disaster"))),
    MyBaseListSummary[SophisticatedBase]( Map("sophisticatedbaseid" -> SophisticatedBaseSummary("super concise")))
  )
  val sophisticatedJson = sphisticatedList.asJson

  println(sophisticatedJson)

  val convertedSophisticatedListDecoder: Result[MyBaseList[SophisticatedBase]] = sophisticatedJson.as[MyBaseList[SophisticatedBase]]

  println(convertedSophisticatedListDecoder)

  convertedSophisticatedListDecoder match {
    case Left(failure) => println(failure)
    case Right(list) => println(list)
  }
}


It is still essential to make sure every case class inheriting a particular trait has a different number of fields from the others inheriting the same trait, or it at least differs with the other case classes in the naming of those fields.

Otherwise, the decoder would not do its job correctly and decode the json as the first case class matching it in the decoder function, instead of the correct intended one.