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?
µ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)