Search code examples
scalaserializationupickle

Serializing polymorphic types with µPickle


I am reading documentation for µPickle and searching the internet, but I was not able to find any mentions of one feature which is quite basic and I remember having it documented for perhaps all serialization libraries I was using before (Jackson, Prickle ...): polymorphic types. The only documentation I have found is for sealed traits / classes. Consider following code:

import upickle.default._

trait Base

object Base{
  implicit val rw: ReadWriter[Base] = ReadWriter.merge(C1.rw, C2.rw)
}
object C1 {
  implicit val rw: ReadWriter[C1] = macroRW
}
object C2 {
  implicit val rw: ReadWriter[C2] = macroRW
}
case class C1(x: Int) extends Base
case class C2(s: String) extends Base

object Main extends App {
  val c1: Base = new C1(0)
  val c2: Base = new C2("X")

  val c1String = write(c1)
  val c2String = write(c2)
  println("c1 " + c1String)
  println("c2 " + c2String)

}

This code would work if I changed trait Base to sealed trait Base. I am fine with the requirement to list all derived classes in the serializer, this is what the other libraries I have mentioned required as well, but it is not always possible or desirable to have multiple large classes in one source file so that the base can be sealed. How can one serialize polymorphic types with uPickle if the base is not sealed?


Solution

  • µPickle works at compile time (macros work at compile time). In order to derive type class instance for trait having instances for subclasses you should know all trait subclasses at compile time. This is possible only for sealed trait (via knownDirectSubclasses https://github.com/lihaoyi/upickle/blob/master/implicits/src/upickle/implicits/internal/Macros.scala#L124 ).

    http://www.lihaoyi.com/upickle/#SupportedTypes

    Supported Types

    Out of the box, uPickle supports writing and reading the following types:

    • Boolean, Byte, Char, Short, Int, Long, Float, Double
    • Tuples from 1 to 22
    • Immutable Seq, List, Vector, Set, SortedSet, Option, Array, Maps, and all other collections with a reasonable CanBuildFrom implementation
    • Duration, Either
    • Stand-alone case classes and case objects, and their generic equivalents,
    • Non-generic case classes and case objects that are part of a sealed trait or sealed class hierarchy
    • sealed trait and sealed classes themselves, assuming that all subclasses are picklable
    • UUIDs
    • null

    As you can see only sealed traits are supported.


    Workaround is to have sealed traits in multiple source files and common parent trait with custom pickler.

      trait Base
    
      object Base {
        implicit val rw: ReadWriter[Base] = readwriter[ujson.Value].bimap[Base]({
          case c: Base1 => writeJs(c)
          case c: Base2 => writeJs(c)
        },
          s => Try(read[Base1](s)).getOrElse(read[Base2](s))
        )
      }
    
      sealed trait Base1 extends Base
      object Base1 {
        implicit val rw: ReadWriter[Base1] = ReadWriter.merge(C1.rw, C11.rw)
      }
    
      case class C1(x: Int) extends Base1
      object C1 {
        implicit val rw: ReadWriter[C1] = macroRW
      }
    
      case class C11(x: Int) extends Base1
      object C11 {
        implicit val rw: ReadWriter[C11] = macroRW
      }
    
      sealed trait Base2 extends Base
      object Base2 {
        implicit val rw: ReadWriter[Base2] = ReadWriter.merge(C2.rw, C22.rw)
      }
    
      case class C2(s: String) extends Base2
      object C2 {
        implicit val rw: ReadWriter[C2] = macroRW
      }
    
      case class C22(s: String) extends Base2
      object C22 {
        implicit val rw: ReadWriter[C22] = macroRW
      }
    
      val c1: Base = new C1(0)
      val c2: Base = new C2("X")
    
      val c1String = write(c1)
      val c2String = write(c2)
      println("c1 " + c1String) // c1 {"$type":"App.C1","x":0}
      println("c2 " + c2String) // c2 {"$type":"App.C2","s":"X"}
    
      println(read[Base](c1String)) // C1(0)
      println(read[Base](c2String)) // C2(X)