uPickle (pronounced micro-pickle) is a lightweight JSON and binary (MessagePack) serialization library for Scala. It's key features are:
This documentation is meant as a thorough reference to this library. For a hands-on introduction, take a look at the following blog post:
"com.lihaoyi" %% "upickle" % "4.0.2" // SBT
ivy"com.lihaoyi::upickle:4.0.2" // Mill
And then you can immediately start writing and reading common Scala objects to strings:
import upickle.default._
write(1) ==> "1"
write(Seq(1, 2, 3)) ==> "[1,2,3]"
read[Seq[Int]]("[1,2,3]") ==> List(1, 2, 3)
write((1, "omg", true)) ==> """[1,"omg",true]"""
read[(Int, String, Boolean)]("""[1,"omg",true]""") ==> (1, "omg", true)
Or to compact byte arrays, using the MessagePack format:
import upickle.default._
writeBinary(1) ==> Array(1)
writeBinary(Seq(1, 2, 3)) ==> Array(0x93.toByte, 1, 2, 3)
readBinary[Seq[Int]](Array[Byte](0x93.toByte, 1, 2, 3)) ==> List(1, 2, 3)
val serializedTuple = Array[Byte](0x93.toByte, 1, 0xa3.toByte, 111, 109, 103, 0xc3.toByte)
writeBinary((1, "omg", true)) ==> serializedTuple
readBinary[(Int, String, Boolean)](serializedTuple) ==> (1, "omg", true)
For ScalaJS applications, use this dependencies instead:
"com.lihaoyi" %%% "upickle" % "4.0.2" // SBT
ivy"com.lihaoyi::upickle::4.0.2" // Mill
uPickle supports Scala 2.12, 2.13 and 3.1+
This is a non-comprehensive list of what the most commonly-used types pickle to using uPickle. To begin, let's import upickle
import upickle.default._
Booleans are serialized as JSON booleans
write(true: Boolean) ==> "true"
write(false: Boolean) ==> "false"
Numbers are serialized as JSON numbers
write(12: Int) ==> "12"
write(12: Short) ==> "12"
write(12: Byte) ==> "12"
write(Int.MaxValue) ==> "2147483647"
write(Int.MinValue) ==> "-2147483648"
write(12.5f: Float) ==> "12.5"
write(12.5: Double) ==> "12.5"
Except for Long
s, which too large for Javascript. These are
serialized as JSON Strings, keeping the interchange format compatible
with the browser's own JSON parser, which provides the best
performance in Scala.js
write(12: Long) ==> "12"
write(4000000000000L: Long) ==> "4000000000000"
// large longs are written as strings, to avoid floating point rounding
write(9223372036854775807L: Long) ==> "\"9223372036854775807\""
Special values of Double
s and Float
s are also
serialized as Strings
write(1.0/0: Double) ==> "\"Infinity\""
write(Float.PositiveInfinity) ==> "\"Infinity\""
write(Float.NegativeInfinity) ==> "\"-Infinity\""
Both Char
s and String
s are serialized as Strings
write('o') ==> "\"o\""
write("omg") ==> "\"omg\""
Array
s and most immutable collections are serialized as JSON lists
write(Array.empty[Int]) ==> "[]"
write(Array(1, 2, 3)) ==> "[1,2,3]"
// You can pass in an `indent` parameter to format it nicely
write(Array.empty[Int], indent = 4) ==> "[]"
write(Array(1, 2, 3), indent = 4) ==>
"""[
| 1,
| 2,
| 3
|]""".stripMargin
write(Seq(1, 2, 3)) ==> "[1,2,3]"
write(Vector(1, 2, 3)) ==> "[1,2,3]"
write(List(1, 2, 3)) ==> "[1,2,3]"
import collection.immutable.SortedSet
write(SortedSet(1, 2, 3)) ==> "[1,2,3]"
Option
s are serialized as nullable values
write(Some(1)) ==> "1"
write(None) ==> "null"
Either
s are serialized as JSON lists with 2 elements, with the first
element a number indicating Left
(0) or Right
(1), and the second
containing the value.
write(Left(42): Either[Int, String]) ==> "[0,42]"
write(Right("foo"): Either[Int, String]) ==> """[1,"foo"]"""
Tuples of all sizes (1-22) are serialized as heterogenous JSON lists
write((1, "omg")) ==> """[1,"omg"]"""
write((1, "omg", true)) ==> """[1,"omg",true]"""
Map
s with primitive keys are serialized into JSON dictionaries,
while Map
with complex keys are serialized into lists of
2-element tuples
write(Map(1 -> 2, 3 -> 4)) ==> """{"1":2,"3":4}"""
write(Map("hello" -> "world")) ==> """{"hello":"world"}"""
write(Map(Seq(1, 2) -> Seq(3, 4))) ==> """[[[1,2],[3,4]]]"""
write(Map.empty[Int, Int]) ==> """{}"""
write(Map(Seq.empty[Int] -> Seq.empty[Int])) ==> """[[[],[]]]"""
write(Map(Seq.empty[Int] -> Seq.empty[Int]), indent = 4) ==>
"""[
| [
| [],
| []
| ]
|]""".stripMargin
write(Map.empty[Int, Int], indent = 4) ==> """{}"""
Case classes of sizes 1-64 are serialized as JSON dictionaries with the keys being the names of each field. To begin with, you need to define a serializer in the Case Class's companion object:
import upickle.default.{ReadWriter => RW, macroRW}
After that, you can begin serializing that case class.
case class Thing(myFieldA: Int, myFieldB: String)
object Thing{
implicit val rw: RW[Thing] = macroRW
}
case class OptionThing(myFieldA: Option[Int] = None, myFieldB: Option[String] = None)
object OptionThing{
implicit val rw: RW[OptionThing] = macroRW
}
case class Big(i: Int, b: Boolean, str: String, c: Char, t: Thing)
object Big{
implicit val rw: RW[Big] = macroRW
}
import upickle._
write(Thing(1, "gg")) ==> """{"myFieldA":1,"myFieldB":"gg"}"""
read[Thing]("""{"myFieldA":1,"myFieldB":"gg"}""") ==> Thing(1, "gg")
write(Big(1, true, "lol", 'Z', Thing(7, ""))) ==>
"""{"i":1,"b":true,"str":"lol","c":"Z","t":{"myFieldA":7,"myFieldB":""}}"""
write(Big(1, true, "lol", 'Z', Thing(7, "")), indent = 4) ==>
"""{
| "i": 1,
| "b": true,
| "str": "lol",
| "c": "Z",
| "t": {
| "myFieldA": 7,
| "myFieldB": ""
| }
|}""".stripMargin
write(Big(1, true, "lol", 'Z', Thing(7, "")), indent = 4, sortKeys = true) ==>
"""{
| "b": true,
| "c": "Z",
| "i": 1,
| "str": "lol",
| "t": {
| "myFieldA": 7,
| "myFieldB": ""
| }
|}""".stripMargin
Sealed hierarchies are serialized as tagged values, the serialized object tagged with the full name of the instance's class:
sealed trait IntOrTuple
object IntOrTuple{
implicit val rw: RW[IntOrTuple] = RW.merge(IntThing.rw, TupleThing.rw)
}
case class IntThing(i: Int) extends IntOrTuple
object IntThing{
implicit val rw: RW[IntThing] = macroRW
}
case class TupleThing(name: String, t: (Int, Int)) extends IntOrTuple
object TupleThing{
implicit val rw: RW[TupleThing] = macroRW
}
write(IntThing(1)) ==> """{"$type":"IntThing","i":1}"""
write(TupleThing("naeem", (1, 2))) ==>
"""{"$type":"TupleThing","name":"naeem","t":[1,2]}"""
// You can read tagged value without knowing its
// type in advance, just use type of the sealed trait
read[IntOrTuple]("""{"$type":"IntThing","i":1}""") ==> IntThing(1)
Serializability is recursive; you can serialize a type only if all its members are serializable. That means that collections, tuples and case-classes made only of serializable members are themselves serializable
case class Foo(i: Int)
object Foo{
implicit val rw: RW[Foo] = macroRW
}
case class Bar(name: String, foos: Seq[Foo])
object Bar{
implicit val rw: RW[Bar] = macroRW
}
write((((1, 2), (3, 4)), ((5, 6), (7, 8)))) ==>
"""[[[1,2],[3,4]],[[5,6],[7,8]]]"""
write(Seq(Thing(1, "g"), Thing(2, "k"))) ==>
"""[{"myFieldA":1,"myFieldB":"g"},{"myFieldA":2,"myFieldB":"k"}]"""
write(Bar("bearrr", Seq(Foo(1), Foo(2), Foo(3)))) ==>
"""{"name":"bearrr","foos":[{"i":1},{"i":2},{"i":3}]}"""
In Scala 3, you can use the derives
keyword on standalone
case class
es, sealed trait
hierarchies, and
enum
s:
case class Dog(name: String, age: Int) derives ReadWriter
upickle.default.write(Dog("Ball", 2)) ==> """{"name":"Ball","age":2}"""
upickle.default.read[Dog]("""{"name":"Ball","age":2}""") ==> Dog("Ball", 2)
sealed trait Animal derives ReadWriter
case class Person(name: String, address: String, age: Int = 20) extends Animal
case class Cat(name: String, owner: Person) extends Animal
case object Cthulu extends Animal
upickle.default.write(Person("Peter", "Ave 10")) ==>
"""{"$type":"Person","name":"Peter","address":"Ave 10"}"""
upickle.default.read[Animal]("""{"$type":"Person","name":"Peter","address":"Ave 10"}""") ==>
Person("Peter", "Ave 10")
upickle.default.write(Cthulu) ==> "\"Cthulu\""
upickle.default.read[Animal]("\"Cthulu\"") ==> Cthulu
enum SimpleEnum derives ReadWriter:
case A, B
upickle.default.write(SimpleEnum.A) ==> "\"A\""
upickle.default.read[SimpleEnum]("\"A\"") ==> SimpleEnum.A
enum ColorEnum(val rgb: Int) derives ReadWriter:
case Red extends ColorEnum(0xFF0000)
case Green extends ColorEnum(0x00FF00)
case Blue extends ColorEnum(0x0000FF)
case Mix(mix: Int) extends ColorEnum(mix)
case Unknown(mix: Int) extends ColorEnum(0x000000)
Enclosing("test", SimpleEnum.A, Some(SimpleEnum.B)),
"""{"str":"test","simple1":"A","simple2":"B"}"""
Note that you only need to put the derives
keyword on the
enum
s or sealed trait
s, and not on all the
individual case class
es or case object
s.
Also, note that for enum
s, the short un-qualified name is used
for the type key, rather than the fully qualified name with package path.
This is because unlike sealed trait
s, enum
s enforce
that every case is in the same flat namespace, and thus the short name is
enough to dis-ambiguate them. This behavior can be overriden with the
@key("...")
annotation in both cases, if a different type key
is desired.
Apart from reading & writing java.lang.String
s, allows you to easily
read from alternate sources such as CharSequence
s, Array[Byte]
s,
java.io.File
s and java.nio.file.Path
s:
import upickle.default._
val original = """{"myFieldA":1,"myFieldB":"gg"}"""
read[Thing](original) ==> Thing(1, "gg")
read[Thing](original: CharSequence) ==> Thing(1, "gg")
read[Thing](original.getBytes) ==> Thing(1, "gg")
import upickle.default._
val original = """{"myFieldA":1,"myFieldB":"gg"}"""
import java.nio.file.Files
val f = Files.createTempFile("", "")
Files.write(f, original.getBytes)
read[Thing](f) ==> Thing(1, "gg")
read[Thing](f.toFile) ==> Thing(1, "gg")
Reading from large files is automatically streamed so you do not read the
entire file into memory. You can use writeTo
to serialize your
data to an arbitrary java.io.Writer
/java.io.OutputStream
:
this can be streamed directly to files or over the network without having
to accumulate the serialized JSON in memory.
write(Bar(null, Seq(Foo(1), null, Foo(3)))) ==>
"""{"name":null,"foos":[{"i":1},null,{"i":3}]}"""
uPickle only throws exceptions on unpickling; if a pickler is
properly defined, serializing a data structure to a String
should never throw an exception.
All these examples can be similarly serialized to MessagePack-formatted binaries, in the same way: JSON booleans become MessagePack booleans, lists become MessagePack lists, and so on. Reading and writing MessagePack binary data is typically significantly faster than reading and writing JSON, and the serialized data is also significantly smaller.
If a field is missing upon deserialization, uPickle uses the default value if one exists
read[FooDefault]("{}") ==> FooDefault(10, "lol")
read[FooDefault]("""{"i": 123}""") ==> FooDefault(123,"lol")
If a field at serialization time has the same value as the default, uPickle leaves it out of the serialized blob
write(FooDefault(i = 11, s = "lol")) ==> """{"i":11}"""
write(FooDefault(i = 10, s = "lol")) ==> """{}"""
write(FooDefault()) ==> """{}"""
This allows you to make schema changes gradually, assuming you have
already pickled some data and want to add new fields to the case classes
you pickled. Simply give the new fields a default value (e.g.
""
for Strings, or wrap it in an Option[T]
and
make the default None
) and uPickle will happily read the
old data, filling in the missing field using the default value.
You can enable serialization of default values a per-field or per-case-class basis
using the @upickle.implicits.serializeDefaults(true)
annotation on that field or case class
, or globally on a
Custom Configuration via override def serializeDefaults = true
Out of the box, uPickle supports writing and reading the following types:
Boolean
, Byte
, Char
, Short
,
Int
, Long
, Float
, Double
Tuple
s from 1 to 22Seq
, List
, Vector
, Set
,
SortedSet
, Option
, Array
, Map
s, and all
other collections with a reasonable CanBuildFrom
implementationDuration
, Either
case class
es and case object
s, and
their generic equivalents,case class
es and case object
s that
are part of a sealed trait
or sealed class
hierarchysealed trait
and sealed class
es themselves,
assuming that all subclasses are picklableUUID
snull
Readability/writability is recursive: a container such as a Tuple
or case class
is only readable if all its contents are
readable, and only writable if all its contents are writable. That means
that you cannot serialize a List[Any]
, since uPickle doesn't
provide a generic way of serializing Any
. Case classes are only
serializable up to 64 fields.
Case classes are de-serialized using the apply
method on their
companion objects. sealed
hierarchies
are serialized as tagged unions: whatever the serialization of the
actual object, together with the partially-qualified name of its class
in a "$type": "..."
field, so
the correct class in the sealed hierarchy can be reconstituted later.
That concludes the list of supported types. Anything else is not supported by default, but you can add support using Custom Picklers
The following common operations are available on any uPickle module,
e.g. upickle.default
or upickle.legacy
:
trait Api
extends upickle.core.Types
with implicits.Readers
with implicits.Writers
with implicits.CaseClassReadWriters
with WebJson
with JsReadWriters
with MsgReadWriters
with Annotator{
private def maybeSortKeysTransform[T: Writer, V](t: T,
sortKeys: Boolean,
f: Visitor[_, V]): V = {
BufferedValue.maybeSortKeysTransform(implicitly[Writer[T]], t, sortKeys, f)
}
/**
* Reads the given MessagePack input into a Scala value
*/
def readBinary[T: Reader](s: upack.Readable, trace: Boolean = false): T = {
TraceVisitor.withTrace(trace, reader[T])(s.transform(_))
}
/**
* Reads the given JSON input into a Scala value
*/
def read[T: Reader](s: ujson.Readable, trace: Boolean = false): T = {
TraceVisitor.withTrace(trace, reader[T])(s.transform(_))
}
def reader[T: Reader] = implicitly[Reader[T]]
/**
* Write the given Scala value as a JSON string
*/
def write[T: Writer](t: T,
indent: Int = -1,
escapeUnicode: Boolean = false,
sortKeys: Boolean = false): String = {
maybeSortKeysTransform(t, sortKeys, ujson.StringRenderer(indent, escapeUnicode)).toString
}
/**
* Write the given Scala value as a MessagePack binary
*/
def writeBinary[T: Writer](t: T,
sortKeys: Boolean = false): Array[Byte] = {
maybeSortKeysTransform(t, sortKeys, new upack.MsgPackWriter(new ByteArrayOutputStream())).toByteArray
}
/**
* Write the given Scala value as a JSON struct
*/
def writeJs[T: Writer](t: T): ujson.Value = transform(t).to[ujson.Value]
/**
* Write the given Scala value as a MessagePack struct
*/
def writeMsg[T: Writer](t: T): upack.Msg = transform(t).to[upack.Msg]
/**
* Write the given Scala value as a JSON string to the given Writer
*/
def writeTo[T: Writer](t: T,
out: java.io.Writer,
indent: Int = -1,
escapeUnicode: Boolean = false,
sortKeys: Boolean = false): Unit = {
maybeSortKeysTransform(t, sortKeys, new ujson.Renderer(out, indent = indent, escapeUnicode))
}
def writeToOutputStream[T: Writer](t: T,
out: java.io.OutputStream,
indent: Int = -1,
escapeUnicode: Boolean = false,
sortKeys: Boolean = false): Unit = {
maybeSortKeysTransform(t, sortKeys, new ujson.BaseByteRenderer(out, indent = indent, escapeUnicode))
}
def writeToByteArray[T: Writer](t: T,
indent: Int = -1,
escapeUnicode: Boolean = false,
sortKeys: Boolean = false): Array[Byte] = {
val out = new java.io.ByteArrayOutputStream()
writeToOutputStream(t, out, indent, escapeUnicode, sortKeys)
out.toByteArray
}
/**
* Write the given Scala value as a JSON string via a `geny.Writable`
*/
def stream[T: Writer](t: T,
indent: Int = -1,
escapeUnicode: Boolean = false,
sortKeys: Boolean = false): geny.Writable = new geny.Writable{
override def httpContentType = Some("application/json")
def writeBytesTo(out: java.io.OutputStream) = {
maybeSortKeysTransform(t, sortKeys, new ujson.BaseByteRenderer(out, indent = indent, escapeUnicode))
}
}
/**
* Write the given Scala value as a MessagePack binary to the given OutputStream
*/
def writeBinaryTo[T: Writer](t: T,
out: java.io.OutputStream,
sortKeys: Boolean = false): Unit = {
streamBinary[T](t, sortKeys = sortKeys).writeBytesTo(out)
}
def writeBinaryToByteArray[T: Writer](t: T,
sortKeys: Boolean = false): Array[Byte] = {
val out = new java.io.ByteArrayOutputStream()
streamBinary[T](t, sortKeys = sortKeys).writeBytesTo(out)
out.toByteArray
}
/**
* Write the given Scala value as a MessagePack binary via a `geny.Writable`
*/
def streamBinary[T: Writer](t: T, sortKeys: Boolean = false): geny.Writable = new geny.Writable{
override def httpContentType = Some("application/octet-stream")
def writeBytesTo(out: java.io.OutputStream) = maybeSortKeysTransform(t, sortKeys, new upack.MsgPackWriter(out))
}
def writer[T: Writer] = implicitly[Writer[T]]
def readwriter[T: ReadWriter] = implicitly[ReadWriter[T]]
case class transform[T: Writer](t: T) extends upack.Readable with ujson.Readable {
def transform[V](f: Visitor[_, V]): V = writer[T].transform(t, f)
def to[V](f: Visitor[_, V]): V = transform(f)
def to[V](implicit f: Reader[V]): V = transform(f)
}
/**
* Mark a `ReadWriter[T]` as something that can be used as a key in a JSON
* dictionary, such that `Map[T, V]` serializes to `{"a": "b", "c": "d"}`
* rather than `[["a", "b"], ["c", "d"]]`
*/
def stringKeyRW[T](readwriter: ReadWriter[T]): ReadWriter[T] = {
new ReadWriter.Delegate[T](readwriter) {
override def isJsonDictKey = true
def write0[R](out: Visitor[_, R], v: T): R = readwriter.write0(out, v)
}
}
/**
* Mark a `Writer[T]` as something that can be used as a key in a JSON
* dictionary, such that `Map[T, V]` serializes to `{"a": "b", "c": "d"}`
* rather than `[["a", "b"], ["c", "d"]]`
*/
def stringKeyW[T](readwriter: Writer[T]): Writer[T] = new Writer[T]{
override def isJsonDictKey = true
def write0[R](out: Visitor[_, R], v: T): R = readwriter.write0(out, v)
}
import upickle.default._
case class Wrap(i: Int)
implicit val fooReadWrite: ReadWriter[Wrap] =
readwriter[Int].bimap[Wrap](_.i, Wrap(_))
write(Seq(Wrap(1), Wrap(10), Wrap(100))) ==> "[1,10,100]"
read[Seq[Wrap]]("[1,10,100]") ==> Seq(Wrap(1), Wrap(10), Wrap(100))
You can use the readwriter[T].bimap[V]
function to create a pickler
that reads/writes a type V
, using the pickler for type T
,
by providing a conversion function between them.
The type you are .bimap
ing to doesn't need to be a case class,
or be pickleable in any way, as long as the type you are
.bimap
ing from is pickleable. The following example demonstrates
using .bimap
to define a serializer for non-case Scala class
class CustomThing2(val i: Int, val s: String)
object CustomThing2 {
implicit val rw: RW[CustomThing2] = upickle.default.readwriter[String].bimap[CustomThing2](
x => s"${x.i} ${x.s}",
str => {
val Array(i, s) = str.split(" ", 2)
new CustomThing2(i.toInt, s)
}
)
}
Note that when writing custom picklers, it is entirely up to you to get it right, e.g. making sure that an object that gets round-trip pickled/unpickled comes out the same as when it started.
Lastly, if you want more control over exactly how something is serialized,
you can use readwriter[Js.Value].bimap
to give yourself access to
the raw JSON AST:
import upickle.default._
case class Bar(i: Int, s: String)
implicit val fooReadWrite: ReadWriter[Bar] =
readwriter[ujson.Value].bimap[Bar](
x => ujson.Arr(x.s, x.i),
json => new Bar(json(1).num.toInt, json(0).str)
)
write(Bar(123, "abc")) ==> """["abc",123]"""
read[Bar]("""["abc",123]""") ==> Bar(123, "abc")
uPickle allows you to specify the key that a field is serialized with
via a @key
annotation
case class KeyBar(@upickle.implicits.key("hehehe") kekeke: Int)
object KeyBar{
implicit val rw: RW[KeyBar] = macroRW
}
write(KeyBar(10)) ==> """{"hehehe":10}"""
read[KeyBar]("""{"hehehe": 10}""") ==> KeyBar(10)
Practically, this is useful if you want to rename the field within your
Scala code while still maintaining backwards compatibility with
previously-pickled objects. Simple rename the field and add a
@key("...")
with the old name so uPickle can continue to work
with the old objects correctly.
You can also use @key
to change the name used when pickling
the case class itself. Normally case classes are pickled without their
name, but an exception is made for members of sealed hierarchies which
are tagged with their fully-qualified name. uPickle allows you to use
@key
to override what the class is tagged with:
sealed trait A
object A{
implicit val rw: RW[A] = RW.merge(B.rw, macroRW[C.type])
}
@upickle.implicits.key("Bee") case class B(i: Int) extends A
object B{
implicit val rw: RW[B] = macroRW
}
case object C extends A
write(B(10)) ==> """{"$type":"Bee","i":10}"""
read[B]("""{"$type":"Bee","i":10}""") ==> B(10)
This is useful in cases where:
You can also use @key
to change the key used when pickling a
member of a sealed hierarchy. The default key is $type
, as seen
above, but you can change it by annotating the sealed trait
:
@upickle.implicits.key("_tag")
sealed trait Tag
case class ATag(i: Int) extends Tag
object ATag {
implicit val rw: RW[ATag] = macroRW
}
write(ATag(11)) ==>
"""{"_tag":"ATag","i":11}"""
read[ATag]("""{"_tag":"ATag","i":11}""") ==>
ATag(11)
This is useful in cases where you need to override uPickle's default behavior, for example to preserve compatibility with another JSON library.
By default, serializing a Map[K, V]
generates a nested
array-of-arrays. This is because not all types K
can be
easily serialized into JSON strings, so keeping them as nested tuples
preserves the structure of the serialized K
values:
import upickle.default._
case class FooId(x: Int)
implicit val fooRW: ReadWriter[FooId] = readwriter[Int].bimap[FooId](_.x, FooId(_))
write(FooId(123)) ==> "123"
read[FooId]("123") ==> FooId(123)
write(Map(FooId(123) -> "hello", FooId(456) -> "world")) ==>
"""[[123,"hello"],[456,"world"]]"""
read[Map[FooId, String]]("""[[123,"hello"],[456,"world"]]""") ==>
Map(FooId(123) -> "hello", FooId(456) -> "world")
For types of K
that you want to serialize to JSON strings in
JSON dictionaries, you can wrap your ReadWriter[K]
in a
stringKeyRW
.
import upickle.default._
case class FooId(x: Int)
implicit val fooRW: ReadWriter[FooId] = stringKeyRW(readwriter[Int].bimap[FooId](_.x, FooId(_)))
write(FooId(123)) ==> "123"
read[FooId]("123") ==> FooId(123)
write(Map(FooId(123) -> "hello", FooId(456) -> "world")) ==>
"""{"123":"hello","456":"world"}"""
read[Map[FooId, String]]("""{"123":"hello","456":"world"}""") ==>
Map(FooId(123) -> "hello", FooId(456) -> "world")
Note that this only works for types K
which serialize to JSON
primitives: numbers, strings, booleans, and so on. Types of K
that serialize to complex structures like JSON arrays or dictionaries
are unsupported for use a JSON dictionary keys.
Older versions of uPickle serialized almost all Map[K, V]
s to
nested arrays. Data already serialized in that format is forwards-compatible
with the current implementation of uPickle, which can read both nested-json-arrays
and json-dictionary formats without issue.
These subtleties around deciding between nested-json-array v.s. json-dictionary
formats only apply to uPickle's JSON backend. When serializing to MessagePack
using upickle.default.writeBinary
or upickle.default.writeMsg
,
it always uses the dictionary-based format, since MessagePack does not have
the restriction that dictionary keys can only be strings:
import upickle.default._
write(Map(Seq(1) -> 1, Seq(1, 2) -> 3, Seq(1, 2, 3) -> 6)) ==> "[[[1],1],[[1,2],3],[[1,2,3],6]]"
read[Map[Seq[Int], Int]]("[[[1],1],[[1,2],3],[[1,2,3],6]]") ==>
Map(Seq(1) -> 1, Seq(1, 2) -> 3, Seq(1, 2, 3) -> 6)
writeMsg(Map(Seq(1) -> 1, Seq(1, 2) -> 3, Seq(1, 2, 3) -> 6)) ==>
upack.Obj(
upack.Arr(upack.Int32(1)) -> upack.Int32(1),
upack.Arr(upack.Int32(1), upack.Int32(2)) -> upack.Int32(3),
upack.Arr(upack.Int32(1), upack.Int32(2), upack.Int32(3)) -> upack.Int32(6)
)
readBinary[Map[Seq[Int], Int]](
upack.Obj(
upack.Arr(upack.Int32(1)) -> upack.Int32(1),
upack.Arr(upack.Int32(1), upack.Int32(2)) -> upack.Int32(3),
upack.Arr(upack.Int32(1), upack.Int32(2), upack.Int32(3)) -> upack.Int32(6)
)
) ==>
Map(Seq(1) -> 1, Seq(1, 2) -> 3, Seq(1, 2, 3) -> 6)
The full list of annotations that can be applied to case class
es
and sealed trait
s is below:
package upickle.implicits
import scala.annotation.StaticAnnotation
/**
* Annotation for control over the strings used to serialize your data structure. Can
* be applied in three ways:
*
* 1. To individual fields, in which case it overrides the JSON object fieldname
*
* 2. To `case class`es which are part of `sealed trait`s, where it overrides
* the value of the `"$type": "foo"` discriminator field
*
* 2. To `sealed trait`s themselves, where it overrides
* the key of the `"$type": "foo"` discriminator field
*/
class key(s: String) extends StaticAnnotation
/**
* Annotation for fine-grained control of the `def serializeDefaults` configuration
* on the upickle bundle; can be applied to individual fields or to entire `case class`es,
* with finer-grained application taking precedence
*/
class serializeDefaults(s: Boolean) extends StaticAnnotation
/**
* Annotation for fine-grained control of the `def allowUnknownKeys` configuration
* on the upickle bundle; can be applied to individual `case class`es, taking precedence
* over upickle pickler-level configuration
*/
class allowUnknownKeys(b: Boolean) extends StaticAnnotation
Often, there will be times that you want to customize something on a
project-wide level. uPickle provides hooks in letting you subclass the
upickle.Api
trait to create your own bundles apart from the
in-built upickle.default
and upickle.legacy
. These are listed
below in the upickle.core.Config
trait:
trait Config {
/**
* Specifies the name of the field used to distinguish different `case class`es under
* a `sealed trait`. Defaults to `$type`, but can be configured globally by overriding
* [[tagName]], or on a per-`sealed trait` basis via the `@key` annotation
*/
def tagName: String = Annotator.defaultTagKey
/**
* Whether to use the fully-qualified name of `case class`es and `case object`s which
* are part of `sealed trait` hierarchies when serializing them and writing their `$type`
* key. Defaults to `false`, so `$type` key uses the shortest partially-qualified name.
* Can be set to `true` to use their fully-qualified name.
*/
def objectTypeKeyWriteFullyQualified: Boolean = false
/**
* Whether or not to write `case class` keys which match their default values.
* Defaults to `false`, allowing those keys to be omitted. Can be set to `true`
* to always write field values even if they are equal to the default
*/
def serializeDefaults: Boolean = false
/**
* Transform dictionary keys when writing `case class`es when reading. Can
* be overriden to provide custom mappings between Scala field names and JSON
* field names. Needs to be kept in sync with [[objectAttributeKeyWriteMap]]
*
* This customizes the mapping across all `case class`es fields handled by this
* upickle instance. This can be customized on a field-by-field basis using the
* [[upickle.implicits.key]] annotation on the `case class` field
*/
def objectAttributeKeyReadMap(s: CharSequence): CharSequence = s
/**
* Map the name of JSON object fields to Scala `case class` fields during serialization.
* Must be kept in sync with [[objectAttributeKeyReadMap]]
*/
def objectAttributeKeyWriteMap(s: CharSequence): CharSequence = s
/**
* Transforms the value of the `$type` field when writing `sealed trait`s,
* to allow custom mapping between the `case class` name and the `$type` field
* in the generated JSON. Must be kept in sync with [[objectTypeKeyWriteMap]].
*
* * This customizes the mapping across all `case class`es fields handled by this
* * upickle instance. This can be customized on a per-`sealed trait` basis using the
* * [[upickle.implicits.key]] annotation on the `case class`
*/
def objectTypeKeyReadMap(s: CharSequence): CharSequence = s
/**
* Map the name of Scala `case class` type names to JSON `$type` field value during
* serialization. Must be kept in sync with [[objectTypeKeyReadMap]]
*/
def objectTypeKeyWriteMap(s: CharSequence): CharSequence = s
/**
* Whether top-level `Some(t)`s and `None`s are serialized unboxed as `t` or
* `null`, rather than `[t]` or `[]`. This is generally what people expect,
* although it does cause issues where `Some(null)` when serialized and de-serialized
* can become `None`. Can be disabled to use the boxed serialization format
* as 0-or-1-element-arrays, presering round trip-ability at the expense of
* un-intuitiveness and verbosity
*/
def optionsAsNulls: Boolean = true
/**
* Configure whether you want upickle to skip unknown keys during de-serialization
* of `case class`es. Can be overriden for the entire serializer via `override def`, and
* further overriden for individual `case class`es via the annotation
* `@upickle.implicits.allowUnknownKeys(b: Boolean)`
*/
def allowUnknownKeys: Boolean = true
}
The
following example demonstrates how to customize a bundle to
automatically snake_case
all dictionary keys.
object SnakePickle extends upickle.AttributeTagged{
def camelToSnake(s: String) = {
s.replaceAll("([A-Z])","#$1").split('#').map(_.toLowerCase).mkString("_")
}
def snakeToCamel(s: String) = {
val res = s.split("_", -1).map(x => s"${x(0).toUpper}${x.drop(1)}").mkString
s"${s(0).toLower}${res.drop(1)}"
}
override def objectAttributeKeyReadMap(s: CharSequence) =
snakeToCamel(s.toString)
override def objectAttributeKeyWriteMap(s: CharSequence) =
camelToSnake(s.toString)
override def objectTypeKeyReadMap(s: CharSequence) =
snakeToCamel(s.toString)
override def objectTypeKeyWriteMap(s: CharSequence) =
camelToSnake(s.toString)
}
// Default read-writing
upickle.default.write(Thing(1, "gg")) ==>
"""{"myFieldA":1,"myFieldB":"gg"}"""
upickle.default.read[Thing]("""{"myFieldA":1,"myFieldB":"gg"}""") ==>
Thing(1, "gg")
implicit def thingRW: SnakePickle.ReadWriter[Thing] = SnakePickle.macroRW
// snake_case_keys read-writing
SnakePickle.write(Thing(1, "gg")) ==>
"""{"my_field_a":1,"my_field_b":"gg"}"""
SnakePickle.read[Thing]("""{"my_field_a":1,"my_field_b":"gg"}""") ==>
Thing(1, "gg")
uPickle by default serializes Scala None
s to JSON null
s, and Some(t)
s
to unboxed JSON t
s. This is generally what people want, but can cause issues at
some edge cases (e.g. Some(null)
can round trip and become None
).
You can use a more verbose serialization which converts Scala None
s to boxed
JSON []]
s, and Some(t)
s to boxed JSON [t]
s
object BoxedOptionsPickler extends upickle.AttributeTagged {
override def optionsAsNulls = false
}
You can also use a custom configuration to change how 64-bit Long
s are
handled. By default, small longs that can be represented exactly in 64-bit
Double
s are written as raw numbers, while larger values (n > 2^53)
are written as strings. This is to ensure the values are not truncated when
the serialized JSON is then manipulated, e.g. by Javascript which truncates
all large numbers to Doubles. If you wish to always write Longs as Strings,
or always write them as numbers (at risk of truncation), you can do so as
follows:
upickle.default.write(123: Long) ==> "123"
upickle.default.write(Long.MaxValue) ==> "\"9223372036854775807\""
object StringLongs extends upickle.AttributeTagged{
override implicit val LongWriter: Writer[Long] = new Writer[Long] {
def write0[V](out: Visitor[_, V], v: Long) = out.visitString(v.toString, -1)
}
}
StringLongs.write(123: Long) ==> "\"123\""
StringLongs.write(Long.MaxValue) ==> "\"9223372036854775807\""
object NumLongs extends upickle.AttributeTagged{
override implicit val LongWriter: Writer[Long] = new Writer[Long] {
def write0[V](out: Visitor[_, V], v: Long) = out.visitFloat64String(v.toString, -1)
}
}
NumLongs.write(123: Long) ==> "123"
NumLongs.write(Long.MaxValue) ==> "9223372036854775807"
uPickle doesn't currently support:
Any
Most of these limitations are inherent in the fact that ScalaJS does not support reflection, and are unlikely to ever go away. In general, uPickle by default can serialize statically-typed, tree-shaped, immutable data structures. Anything more complex requires Custom Picklers
Due to a bug in the Scala compiler SI-7046, automatic sealed trait
pickling can fail unpredictably. This can be worked around by instead
using the macroRW
and merge
methods to manually specify
which sub-types of a sealed trait to consider when pickling:
sealed trait TypedFoo
object TypedFoo{
import upickle.default._
implicit val readWriter: ReadWriter[TypedFoo] = ReadWriter.merge(
macroRW[Bar], macroRW[Baz], macroRW[Quz],
)
case class Bar(i: Int) extends TypedFoo
case class Baz(s: String) extends TypedFoo
case class Quz(b: Boolean) extends TypedFoo
}
uJson is uPickle's JSON library, which can be used to easily
manipulate JSON source and data structures without converting them into
Scala case-classes. This all lives in the ujson
package. Unlike
many other Scala JSON libraries that come with their own zoo of new
concepts, abstractions, and techniques, uJson has a simple & predictable
JSON API that should be instantly familiar to anyone coming from
scripting languages like Ruby, Python or Javascript.
uJson comes bundled with uPickle, or can be used stand-alone via the following package coordinates:
libraryDependencies += "com.lihaoyi" %% "ujson" % "0.9.6"
You can use ujson
to conveniently construct JSON blobs, either
programmatically:
val json0 = ujson.Arr(
ujson.Obj("myFieldA" -> ujson.Num(1), "myFieldB" -> ujson.Str("g")),
ujson.Obj("myFieldA" -> ujson.Num(2), "myFieldB" -> ujson.Str("k"))
)
val json = ujson.Arr( // The `ujson.Num` and `ujson.Str` calls are optional
ujson.Obj("myFieldA" -> 1, "myFieldB" -> "g"),
ujson.Obj("myFieldA" -> 2, "myFieldB" -> "k")
)
json0 ==> json
Or parsing them from strings, byte arrays or files:
val str = """[{"myFieldA":1,"myFieldB":"g"},{"myFieldA":2,"myFieldB":"k"}]"""
val json = ujson.read(str)
json(0)("myFieldA").num ==> 1
json(0)("myFieldB").str ==> "g"
json(1)("myFieldA").num ==> 2
json(1)("myFieldB").str ==> "k"
ujson.write(json) ==> str
ujson.Js
ASTs are mutable, and can be modified before being re-serialized
to strings:
val str = """[{"myFieldA":1,"myFieldB":"g"},{"myFieldA":2,"myFieldB":"k"}]"""
val json: ujson.Value = ujson.read(str)
json.arr.remove(1)
json(0)("myFieldA") = 1337
json(0)("myFieldB") = json(0)("myFieldB").str + "lols"
You can also use the `_` shorthand syntax to update a JSON value in place, without having to duplicate the whole path:
val str = """[{"myFieldA":1,"myFieldB":"g"},{"myFieldA":2,"myFieldB":"k"}]"""
val json: ujson.Value = ujson.read(str)
json(0)("myFieldA") = _.num + 100
json(1)("myFieldB") = _.str + "lol"
case classes or other Scala data structures can be converted to
ujson.Js
ASTs using upickle.default.writeJs
, and the
ujson.Js
ASTs can be converted back using upickle.default.readJs
or plain upickle.default.read
:
val data = Seq(Thing(1, "g"), Thing(2, "k"))
val json = upickle.default.writeJs(data)
json.arr.remove(1)
json(0)("myFieldA") = 1337
upickle.default.read[Seq[Thing]](json) ==> Seq(Thing(1337, "g"))
uJson comes with some convenient utilities on the `ujson` package:
package object ujson{
def transform[T](t: Readable,
v: upickle.core.Visitor[_, T],
sortKeys: Boolean = false): T = {
BufferedValue.maybeSortKeysTransform(Readable, t, sortKeys, v)
}
/**
* Read the given JSON input as a JSON struct
*/
def read(s: Readable, trace: Boolean = false): Value.Value =
upickle.core.TraceVisitor.withTrace(trace, Value)(transform(s, _))
def copy(t: Value.Value): Value.Value = transform(t, Value)
/**
* Write the given JSON struct as a JSON String
*/
def write(t: Value.Value,
indent: Int = -1,
escapeUnicode: Boolean = false,
sortKeys: Boolean = false): String = {
val writer = new java.io.StringWriter
writeTo(t, writer, indent, escapeUnicode, sortKeys)
writer.toString
}
/**
* Write the given JSON struct as a JSON String to the given Writer
*/
def writeTo(t: Value.Value,
out: java.io.Writer,
indent: Int = -1,
escapeUnicode: Boolean = false,
sortKeys: Boolean = false): Unit = {
transform(t, Renderer(out, indent, escapeUnicode), sortKeys)
}
def writeToOutputStream(t: Value.Value,
out: java.io.OutputStream,
indent: Int = -1,
escapeUnicode: Boolean = false,
sortKeys: Boolean = false): Unit = {
transform(t, new BaseByteRenderer(out, indent, escapeUnicode), sortKeys)
}
def writeToByteArray(t: Value.Value,
indent: Int = -1,
escapeUnicode: Boolean = false,
sortKeys: Boolean = false): Array[Byte] = {
val baos = new java.io.ByteArrayOutputStream
writeToOutputStream(t, baos, indent, escapeUnicode, sortKeys)
baos.toByteArray
}
/**
* Parse the given JSON input, failing if it is invalid
*/
def validate(s: Readable): Unit = transform(s, NoOpVisitor)
/**
* Parse the given JSON input and write it to a string with
* the configured formatting
*/
def reformat(s: Readable,
indent: Int = -1,
escapeUnicode: Boolean = false,
sortKeys: Boolean = false): String = {
val writer = new java.io.StringWriter()
reformatTo(s, writer, indent, escapeUnicode, sortKeys)
writer.toString
}
/**
* Parse the given JSON input and write it to a string with
* the configured formatting to the given Writer
*/
def reformatTo(s: Readable,
out: java.io.Writer,
indent: Int = -1,
escapeUnicode: Boolean = false,
sortKeys: Boolean = false): Unit = {
transform(s, Renderer(out, indent, escapeUnicode), sortKeys)
}
/**
* Parse the given JSON input and write it to a string with
* the configured formatting to the given Writer
*/
def reformatToOutputStream(s: Readable,
out: java.io.OutputStream,
indent: Int = -1,
escapeUnicode: Boolean = false,
sortKeys: Boolean = false): Unit = {
transform(s, new BaseByteRenderer(out, indent, escapeUnicode), sortKeys)
}
def reformatToByteArray(s: Readable,
indent: Int = -1,
escapeUnicode: Boolean = false,
sortKeys: Boolean = false): Array[Byte] = {
val baos = new java.io.ByteArrayOutputStream
reformatToOutputStream(s, baos, indent, escapeUnicode, sortKeys)
baos.toByteArray
}
}
uJson allows you seamlessly convert between any of the following forms you may find your JSON in:
ujson.Js
ASTsString
sCharSequence
sArray[Byte]
s
This is done using the ujson.transform(source, dest)
function:
// It can be used for parsing JSON into an AST
val exampleAst = ujson.Arr(1, 2, 3)
ujson.transform("[1, 2, 3]", Value) ==> exampleAst
// Rendering the AST to a string
ujson.transform(exampleAst, StringRenderer()).toString ==> "[1,2,3]"
// Or to a byte array
ujson.transform(exampleAst, BytesRenderer()).toByteArray ==> "[1,2,3]".getBytes
// Re-formatting JSON, either compacting it
ujson.transform("[1, 2, 3]", StringRenderer()).toString ==> "[1,2,3]"
// or indenting it
ujson.transform("[1, 2, 3]", StringRenderer(indent = 4)).toString ==>
"""[
| 1,
| 2,
| 3
|]""".stripMargin
// `transform` takes any `Transformable`, including byte arrays and files
ujson.transform("[1, 2, 3]".getBytes, StringRenderer()).toString ==> "[1,2,3]"
All transformations from A to B using ujson.transform
happen
in a direct fashion: there are no intermediate JSON ASTs being generated,
and performance is generally very good.
You can use ujson.transform
to validate JSON in a streaming
fashion:
test {
ujson.transform("[1, 2, 3]", NoOpVisitor)
The normal upickle.default.read/write
methods to serialize Scala
data-types is just shorthand for ujson.transform,
using a upickle.default.transform(foo)
as the source or a
upickle.default.reader[Foo]
as the destination:
ujson.transform("[1, 2, 3]", upickle.default.reader[Seq[Int]]) ==>
Seq(1, 2, 3)
ujson.transform(upickle.default.transform(Seq(1, 2, 3)), StringRenderer()).toString ==>
"[1,2,3]"
uJson does not provide any other utilities are JSON that other libraries do: zippers, lenses, combinators, etc.. However, uJson can be used to seamlessly convert between the JSON AST of other libraries! This means if some other library provides a more convenient API for some kind of processing you need to do, you can easily parse to that library's AST, do whatever you need, and convert back after.
As mentioned earlier, conversions are fast and direct, and happen without creating intermediate JSON structures in the process. The following examples demonstrate how to use the conversion modules for Argonaut, Circe, Json4s, and Play Json.
Each example parses JSON from a string into that particular library's JSON AST, manipulates the AST using that library, un-pickles it into Scala data types, then serializes those data types first into that library's AST then back to a string.
libraryDependencies += "com.lihaoyi" %% "ujson-argonaut" % "0.9.6"
Usage
val argJson: argonaut.Json = ArgonautJson(
"""["hello", "world"]"""
)
val updatedArgJson = argJson.withArray(_.map(_.withString(_.toUpperCase)))
val items: Seq[String] = ArgonautJson.transform(
updatedArgJson,
upickle.default.reader[Seq[String]]
)
items ==> Seq("HELLO", "WORLD")
val rewritten = upickle.default.transform(items).to(ArgonautJson)
val stringified = ArgonautJson.transform(rewritten, StringRenderer()).toString
stringified ==> """["HELLO","WORLD"]"""
libraryDependencies += "com.lihaoyi" %% "ujson-circe" % "0.9.6"
Usage
val circeJson: io.circe.Json = CirceJson(
"""["hello", "world"]"""
)
val updatedCirceJson =
circeJson.mapArray(_.map(x => x.mapString(_.toUpperCase)))
val items: Seq[String] = CirceJson.transform(
updatedCirceJson,
upickle.default.reader[Seq[String]]
)
items ==> Seq("HELLO", "WORLD")
val rewritten = upickle.default.transform(items).to(CirceJson)
val stringified = CirceJson.transform(rewritten, StringRenderer()).toString
stringified ==> """["HELLO","WORLD"]"""
libraryDependencies += "com.lihaoyi" %% "ujson-play" % "0.9.6"
Usage
import play.api.libs.json._
val playJson: play.api.libs.json.JsValue = PlayJson(
"""["hello", "world"]"""
)
val updatedPlayJson = JsArray(
for(v <- playJson.as[JsArray].value)
yield JsString(v.as[String].toUpperCase())
)
val items: Seq[String] = PlayJson.transform(
updatedPlayJson,
upickle.default.reader[Seq[String]]
)
items ==> Seq("HELLO", "WORLD")
val rewritten = upickle.default.transform(items).to(PlayJson)
val stringified = PlayJson.transform(rewritten, StringRenderer()).toString
stringified ==> """["HELLO","WORLD"]"""
libraryDependencies += "com.lihaoyi" %% "ujson-json4s" % "0.9.6"
Usage
import org.json4s.JsonAST
val json4sJson: JsonAST.JValue = Json4sJson(
"""["hello", "world"]"""
)
val updatedJson4sJson = JsonAST.JArray(
for(v <- json4sJson.children)
yield JsonAST.JString(v.values.toString.toUpperCase())
)
val items: Seq[String] = Json4sJson.transform(
updatedJson4sJson,
upickle.default.reader[Seq[String]]
)
items ==> Seq("HELLO", "WORLD")
val rewritten = upickle.default.transform(items).to(Json4sJson)
val stringified = Json4sJson.transform(rewritten, StringRenderer()).toString
stringified ==> """["HELLO","WORLD"]"""
uJson lets you convert between third-party ASTs efficiently and with minimal overhead: uJson converts one AST to the other directly and without any temporary compatibility data structures. The following example demonstrates how this is done: we parse a JSON string using Circe, perform some transformation, convert it to a Play-Json AST, perform more transformations, and finally serialize it back to a String and check that both transformations were applied:
val circeJson: io.circe.Json = CirceJson(
"""["hello", "world"]"""
)
val updatedCirceJson =
circeJson.mapArray(_.map(x => x.mapString(_.toUpperCase)))
import play.api.libs.json._
val playJson: play.api.libs.json.JsValue = CirceJson.transform(
updatedCirceJson,
PlayJson
)
val updatedPlayJson = JsArray(
for(v <- playJson.as[JsArray].value)
yield JsString(v.as[String].reverse)
)
val stringified = PlayJson.transform(updatedPlayJson, StringRenderer()).toString
stringified ==> """["OLLEH","DLROW"]"""
uPack is uPickle's MessagePack library, which can be used to easily
manipulate MessagePack source and data structures without converting them into
Scala case-classes. This all lives in the upack
package.
uPack comes bundled with uPickle, or can be used stand-alone via the following package coordinates:
libraryDependencies += "com.lihaoyi" %% "upack" % "0.9.6"
The following basic functions are provided in the upack
package
to let you read and write MessagePack structs:
def transform[T](t: Readable, v: upickle.core.Visitor[_, T]) = t.transform(v)
/**
* Read the given MessagePack input into a MessagePack struct
*/
def read(s: Readable, trace: Boolean = false): Msg = upickle.core.TraceVisitor.withTrace(trace, Msg)(transform(s, _))
def copy(t: Msg): Msg = transform(t, Msg)
/**
* Write the given MessagePack struct as a binary
*/
def write(t: Msg): Array[Byte] = {
transform(t, new MsgPackWriter()).toByteArray
}
/**
* Write the given MessagePack struct as a binary to the given OutputStream
*/
def writeTo(t: Msg, out: java.io.OutputStream): Unit = {
transform(t, new MsgPackWriter(out))
}
def writeToByteArray(t: Msg) = {
val out = new ByteArrayOutputStream()
transform(t, new MsgPackWriter(out))
out.toByteArray
}
/**
* Parse the given MessagePack input, failing if it is invalid
*/
def validate(s: Readable): Unit = transform(s, NoOpVisitor)
MessagePack structs are represented using the upack.Msg
type.
You can construct ad-hoc MessagePack structs using upack.Msg
, and
can similarly parse binary data into upack.Msg
for ad-hoc querying
and manipulation, without needing to bind it to Scala case classes or data
types:
val msg = upack.Arr(
upack.Obj(upack.Str("myFieldA") -> upack.Int32(1), upack.Str("myFieldB") -> upack.Str("g")),
upack.Obj(upack.Str("myFieldA") -> upack.Int32(2), upack.Str("myFieldB") -> upack.Str("k"))
)
val binary: Array[Byte] = upack.write(msg)
val read = upack.read(binary)
assert(msg == read)
You can read/write Scala values to upack.Msg
s using readBinary
/writeMsg
:
val big = Big(1, true, "lol", 'Z', Thing(7, ""))
val msg: upack.Msg = upickle.default.writeMsg(big)
upickle.default.readBinary[Big](msg) ==> big
Or include upack.Msg
s inside Seq
s, case-classes and other
data structures when you read/write them:
val msgSeq = Seq[upack.Msg](
upack.Str("hello world"),
upack.Arr(upack.Int32(1), upack.Int32(2))
)
val binary: Array[Byte] = upickle.default.writeBinary(msgSeq)
upickle.default.readBinary[Seq[upack.Msg]](binary) ==> msgSeq
You can also convert the uPack messages or binaries to ujson.Value
s via
upack.transform
. This can be handy to help debug what's going on in your
binary message data:
val msg = upack.Arr(
upack.Obj(upack.Str("myFieldA") -> upack.Int32(1), upack.Str("myFieldB") -> upack.Str("g")),
upack.Obj(upack.Str("myFieldA") -> upack.Int32(2), upack.Str("myFieldB") -> upack.Str("k"))
)
val binary: Array[Byte] = upack.write(msg)
// Can pretty-print starting from either the upack.Msg structs,
// or the raw binary data
upack.transform(msg, new ujson.StringRenderer()).toString ==>
"""[{"myFieldA":1,"myFieldB":"g"},{"myFieldA":2,"myFieldB":"k"}]"""
upack.transform(binary, new ujson.StringRenderer()).toString ==>
"""[{"myFieldA":1,"myFieldB":"g"},{"myFieldA":2,"myFieldB":"k"}]"""
// Some messagepack structs cannot be converted to valid JSON, e.g.
// they may have maps with non-string keys. These can still be pretty-printed:
val msg2 = upack.Obj(upack.Arr(upack.Int32(1), upack.Int32(2)) -> upack.Int32(1))
upack.transform(msg2, new ujson.StringRenderer()).toString ==> """{"[1,2]":1}"""
Note that such a conversion between MessagePack structs and JSON data is lossy: some MessagePack constructs, such as binary data, cannot be exactly represented in JSON and have to be converted to strings. Thus you should not rely on being able to round-trip data between JSON <-> MessagePack and getting the same thing back, although round tripping data between Scala-data-types <-> JSON and Scala-data-types <-> MessagePack should always work.
Some of the differences between the ways things are serialized in MessagePack and JSON include:
ujson.Str
s if n > 2^53; in MessagePack,
they are represented as upack.Int64
s or upack.UInt64
sArray[Byte]
s in JSON are represented as lists of numbers; in MessagePack,
they are represented as upack.Binary
If you need to construct Scala case classes or other data types from your
MessagePack binary data, you should directly use upickle.default.readBinary
and upickle.default.writeBinary
: these bypass the upack.Msg
struct entirely for the optimal performance.
The uPickle has a small set of benchmarks in bench/
that tests
reading and writing performance of a few common JSON libraries on a small,
somewhat arbitrary workload. The numbers below show how many times each
library could read/write a small data structure in 25 seconds (bigger
numbers better). In some libraries, caching the serializers rather than
re-generating them each read/write also improves performance: that effect
can be seen in the (Cached)
columns.
uPickle runs 30-50% faster than Circe for reads/writes, and ~200% faster than play-json.
Library | Reads | Writes | Reads (Cached) | Write (Cached) |
---|---|---|---|---|
Play Json 2.9.2 | 331 | 296 | 361 | 309 |
Circe 0.13.0 | 517 | 504 | 526 | 502 |
upickle.default 1.3.0 (JSON Strings) | 809 | 728 | 822 | 864 |
upickle.default 1.3.0 (JSON Array[Byte]) | 761 | 706 | 774 | 830 |
upickle.default 1.3.0 (MsgPack Array[Byte]) | 1652 | 1264 | 1743 | 1753 |
As you can see, uPickle's JSON serialization is pretty consistently ~50% faster than Circe for reads and writes, and 100-200% faster than Play-Json, depending on workload.
uPickle's binary MessagePack backend is then another 100% faster than uPickle JSON.
uPickle achieves this speed by avoiding the construction of an
intermediate JSON AST: while most libraries parse from
String -> AST -> CaseClass
, uPickle parses input directly from
String -> CaseClass
. uPickle also provides a ujson.Js
AST that
you can use to manipulate arbitrary JSON, but ujson.Js
plays no
part in parsing things to case-classes and is purely for users who want
to manipulate JSON.
While all libraries are much slower on Scala.js/Node.js than on the JVM, uPickle runs 4-5x as fast as Circe or Play-Json for reads and writes.
Library | Reads | Writes | Reads (Cached) | Write (Cached) |
---|---|---|---|---|
Play Json 2.9.2 | 64 | 81 | 66 | 86 |
Circe 0.13.0 | 98 | 121 | 99 | 99 |
upickle.default 1.3.0 (JSON String) | 76 | 40 | 76 | 44 |
upickle.default 1.3.0 (JSON Array[Byte] | 68 | 107 | 69 | 112 |
upickle.default.web 1.3.0 (JSON String) | 349 | 327 | 353 | 465 |
upickle.default 1.3.0 (MsgPack Array[Byte]) | 85 | 104 | 85 | 110 |
On Scala.js, uPickle's performance is comparable to othersl ike Play JSON or Circe.
However, uPickle also exposes the upickle.default.web
API, allowing you to
use the JS runtime's built-in JSON parser to power serialization and deserialization.
This ends up being 4-6x faster than the Scala-based JSON parsers of all the libraries
compared (uPickle, Play-JSON, Circe)
uJson is a fork of Erik Osheim's excellent [Jawn](https://github.com/non/jawn) JSON library, and inherits a lot of it's performance from Erik's work.
Config#tagName
in Reader.merge
and ReadWriter.merge
#619Option
s, sealed trait
s,
etc.), as well as breaking binary compatibility. Please be
careful upgrading and follow the instructions below.$type
tag used for discriminating sealed trait
cases
#594Options
when serializing JSON
#598apply
method instead of new
#607serializeDefaults
as an annotation
#608.override def objectTypeKeyWriteFullyQualified = true
and
override def optionsAsNulls = false
to your upickle
Custom Configuration. If you are currently using upickle.default
, please define
a Custom Configuration and replace all usage of upickle.default
with it. This will preserve the prior serialization format, allowing a smooth upgradeoverride def objectTypeKeyWriteFullyQualified = true
is both forwards and
backwards compatible. Once your entire system is on uPickle 4.0.0, you can incrementally
disable objectTypeKeyWriteFullyQualified
, and uPickle 4.0.0 will be able to read
the generate JSON whether objectTypeKeyWriteFullyQualified
is true
or false
.override def optionsAsNulls = false
is not forwards or backwards
compatible, and cannot be due to the nature of the change in the serialization
format. If you are unable to do a "big bang" upgrade of your entire system (both
code and serialized data) to uPickle 4.0.0, it may be best to continue using
override def optionsAsNulls = false
for the foreseeable future.
$type
tag with key
annotation (#579)
sortKeys = true
flag that can be passed to upickle.default.write
or ujson.write
, allowing you to ensure the generated JSON has object keys in sorted
order
allowUnknownKeys
to false
globally or on a per-case-class
basis to raise errors if unknown keys are present #548upickle.default.read[Seq[String]]("[12345678901234567890]") ==> Seq("12345678901234567890")
,
for scenarios where you want to read JSON numbers "as is", preserving the
exact string representation, without rounding, truncation, or overflow.ujson.Num
s
#504expected tagged dictionary
error message and make it work for
non-empty dicts #502,
derives
properly handle abstract class
es in Scala 3
#470superTypeWriter
/superTypeReader
#471import given
#472SortedMap
and LinkedHashMap
#479deriving
keyword in Scala 3, on both enum
s
and sealed trait
s #453
ujson.Value
s behave properly as a direct target of reading
and writing operations #436
ujson.Bool
pattern matching is now exhaustive
#461
java.lang
boxed versions
of primitive types #462
Map
s, case object
s,
Unit
, etc.). Please be careful upgrading and follow the instructions
below.
case object
s are now serialized as literal strings
"foo.bar.Qux"
rather than JSON dictionaries
{"$type": "foo.bar.Qux"}
#382. This also
applies to Scala 3 Enums 378
Map
s with primitive keys can now be serialized
as JSON dictionaries {"foo": "bar", "baz": "qux"}
, rather than
as nested tuples [["foo", "bar"], ["baz", "qux"]]
#381. This applies
to numbers, booleans, strings, java.util.UUID
s, BigIntegers
, BigDecimals
,
scala.Symbol
s, scala.concurrent.Duration
, and can be enabled for
user-defined types. See JSON Dictionary Formats for more informationscala.Unit
now serializes as null
rather than {}
Map
s can be deserialized from both JSON list-of-list and JSON dictionary
formats, numbers can be deserialized from strings, and Unit
can be deserialized from
empty JSON dictionaries or nullscase object
s has been removed, due to
confusing bugs in how its implicit macro interacted with other implicit definitions.
You now need to define case object
serializers manually, via
implicit val rw: RW[MyCaseObject.type] = macroRW
MsgPackReader
: properly increment index in ext #370
ArrayIndexOutOfBound
s from parsing and replaced them with ParsingFailedException
sjava.io.InputStream
ujson.Value
and upack.Msg
now support the
geny.Writable interfaceupickle.default.writable
and upickle.default.writableBinary
,
to serialize Scala data types via the
geny.Writable interface
upickle.default.writeBinary
/upickle.default.readBinary
,
or via the standalone upack.read
/upack.write
package.
Binary serialization typically is 50-100% faster than JSON when running
on the JVM.
ujson.Js.Value
, ujson.Js.Obj
, etc. are now just
ujson.Value
, ujson.Obj
Long
64-bit integers are now read/written as JSON numbers
by default; only large values which cannot be precisely stored in
Double
-precision floating point (n > 2^53) are written as strings.
You can revert to the old behavior via a Custom Configuration
with:
override implicit val LongWriter = new Writer[Long] {
def write0[V](out: Visitor[_, V], v: Long) = out.visitInt64(v, -1)
}
upickle.json.*
and upickle.Js.*
have been removed (use
ujson.*
.
escapeUnicode: Boolean = false
flag to ujson.Js#Render
and ujson.Renderer
; pass in false
to rrender unicode
characters verbatim rather than escaping them.Js.Obj
values without
explicitly wrapping values [#230](https://github.com/lihaoyi/upickle/issues/230)JsValue#bool
helper [#227](https://github.com/lihaoyi/upickle/pull/223)
for extracting boolean valuesjson(0)("myFieldA") = _.num + 100
ujson.copy
helperujson.Js.Obj
and Arr
upickle.Js
JSON AST and non/jawn
dependency have been combined into the uJson standalone library.
uJson provides high-performance streaming JSON processing that lets uPickle
parse input strings directly to case classes without an intermediate AST.upickle.Js
objects are now mutable, and had some implicits added
to make Construction less awkward.upickle.default.read
callujson
to
parse JSON coming from files.ujson.writeTo
function for serializing JSON directly
to a java.io.Writer
, rather than creating a String
ujson.write
now takes an optional `sortKeys` flag, if
you want the JSON dictionaries to rendered in a standardized order
BigInt
and BigDecimal
,
thanks to Jisoo ParkJs.Value
s are now serializable, thanks to
Felix Dietzenull
s via a Custom Configurationupickle.key
a case class
derive
on default arguments #143case class
's companion object, potentially speeding up compilation times and runtimes.arr: Seq[Js.Value]
, .obj: Map[String, Js.Value]
, .str: String
and .num: Double
helper methods on Js.Value
to simplify usage as a simple JSON tree.Invalid.Json
and Invalid.Data
now have better exception messages by default, which should simplify debuggingindent
parameter to upickle.default.write
in order to format/indent the JSON nicely across multiple linesequals
/hashCode
in macro (#124), thanks to VoltirMatchErrors
(#101)Reader
s/Writer
s should no longer fail in class extends
clauses (#108)Float.NaN
and Double.NaN
are now properly handled (#123)snake_case
case-class fields during serialization/de-serialization (#120)
derive
subprojectimport upickle.default._
which now renders sealed trait hierarchies as dictionaries with a $type
attribute, and import upickle.legacy._
which does the old-style array-wrapper.upickle.Api
if you wish to customize things further, e.g. changing the type-attribute or changing the rendering of case classes.
java.util.UUID
, which are serialized as strings in the standard format
'Symbol
s are now read/write-able by defaultMap[String, V]
now pickles to a JSON dictionary "key": "value", ...}
. Map[K, V]
for all other K != String
are unchangednull
CanBuildFrom
to serialize a broader range of collections
Unit
/()
Jawn
/JSON.parse
on the two platforms, resulting in a 10-15x speedup for JSON handling.Js.{String, Object, Array, Number}
into Js.{Str, Obj, Arr, Num}
, and made Js.Arr
and Js.Obj
use varargs, to allow for better direct-use.upickle.key("...")
annotation, which allows you to override the default key used (which is the class/parameter name) with a custom one, allowing you to change the class/param name in your code while maintaining compatibility with serialized structuresInvalid.Data
exceptionobject
s are now serialized as {}
rather than []
, better matching the style of case classes{}
the same way as object
sMatchError
s with Invalid.Data
errors
Internals
namespaceimport upickle._
is necessary to use the library
Case0ReadWriter
anymore!
MatchError
s or similar, parse failures now are restricted to subclasses upickle.Invalid
which define different failure modes.