Suma
Suma

Reputation: 34433

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?

Upvotes: 3

Views: 569

Answers (2)

malcolm
malcolm

Reputation: 320

The best approach that I have found is something like this:

      import upickle.default.{macroRW, read, write, ReadWriter as RW, readwriter}

      trait IntOrBool
      case class IntWrapper(i: Int) extends IntOrBool
      case class BoolWrapper(b: Boolean) extends IntOrBool
      given RW[IntWrapper] = macroRW[IntWrapper]
      given RW[BoolWrapper] = macroRW[BoolWrapper]
      given RW[IntOrBool] = readwriter[ujson.Value].bimap[IntOrBool](
        {
          case iw: IntWrapper => writeJs(iw).obj += ("type" -> writeJs("IntWrapper"))
          case bw: BoolWrapper => writeJs(bw).obj += ("type" -> writeJs("BoolWrapper"))
          case t => throw new IllegalArgumentException(s"Unknown type: $t")
        },
        json =>
          json("type").str match {
            case "IntWrapper" => read[IntWrapper](json)
            case "BoolWrapper" => read[BoolWrapper](json)
            case t => throw new IllegalArgumentException(s"Unknown type: $t")
          },
      )

Specifically, this serializes to the regular json form of a type, with an additional type field to help with deserialization. It took me a long time to figure this out and I wish that it were in the readme.

Upvotes: 0

Dmytro Mitin
Dmytro Mitin

Reputation: 51703

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

Upvotes: 3

Related Questions