Fork me on GitHub

µPickle 0.6.5

uPickle (pronounced micro-pickle) is a lightweight JSON serialization library for Scala. It's key features are:

Getting Started

Add the following to your SBT config:

libraryDependencies += "com.lihaoyi" %% "upickle" % "0.6.5"

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]"""

type Tup = (Int, String, Boolean)

read[Tup]("""[1,"omg",true]""") ==> (1, "omg", true)


For ScalaJS applications, use this dependencies instead:

libraryDependencies += "com.lihaoyi" %%% "upickle" % "0.6.5"

Other than that, everything is used the same way. upickle-0.6.5 is only compatible with ScalaJS 0.6.x.

Scala 2.10

uPickle does not support Scala 2.10; only 2.11 and 2.12 are supported



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 Longs, 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\""

Special values of Doubles and Floats are also serialized as Strings

write(1.0/0: Double)              ==> "\"Infinity\""
write(Float.PositiveInfinity)     ==> "\"Infinity\""
write(Float.NegativeInfinity)     ==> "\"-Infinity\""

Both Chars and Strings are serialized as Strings

write('o')                        ==> "\"o\""
write("omg")                      ==> "\"omg\""

Arrays and most immutable collections are serialized as JSON lists

write(Array(1, 2, 3))             ==> "[1,2,3]"

// You can pass in an `indent` parameter to format it nicely
write(Array(1, 2, 3), indent = 4)  ==>
    |    1,
    |    2,
    |    3

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]"

Options are serialized as JSON lists with 0 or 1 element

write(Some(1))                    ==> "[1]"
write(None)                       ==> "[]"

Tuples of all sizes (1-22) are serialized as heterogenous JSON lists

write((1, "omg"))                 ==> """[1,"omg"]"""
write((1, "omg", true))           ==> """[1,"omg",true]"""

Case classes of sizes 1-22 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 def rw: RW[Thing] = macroRW
case class Big(i: Int, b: Boolean, str: String, c: Char, t: Thing)
object Big{
  implicit def 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, ""))) ==>

write(Big(1, true, "lol", 'Z', Thing(7, "")), indent = 4) ==>
    |    "i": 1,
    |    "b": true,
    |    "str": "lol",
    |    "c": "Z",
    |    "t": {
    |        "myFieldA": 7,
    |        "myFieldB": ""
    |    }

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 def rw: RW[IntOrTuple] = RW.merge(,
case class IntThing(i: Int) extends IntOrTuple
object IntThing{
  implicit def rw: RW[IntThing] = macroRW
case class TupleThing(name: String, t: (Int, Int)) extends IntOrTuple
object TupleThing{
  implicit def rw: RW[TupleThing] = macroRW
write(IntThing(1)) ==> """{"$type":"upickle.example.Sealed.IntThing","i":1}"""

write(TupleThing("naeem", (1, 2))) ==>

// You can read tagged value without knowing its
// type in advance, just use type of the sealed trait
read[IntOrTuple]("""{"$type":"upickle.example.Sealed.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 def rw: RW[Foo] = macroRW
case class Bar(name: String, foos: Seq[Foo])
object Bar{
  implicit def rw: RW[Bar] = macroRW
write((((1, 2), (3, 4)), ((5, 6), (7, 8)))) ==>

write(Seq(Thing(1, "g"), Thing(2, "k"))) ==>

write(Bar("bearrr", Seq(Foo(1), Foo(2), Foo(3)))) ==>

Nulls serialize into JSON nulls, as you would expect

write(Bar(null, Seq(Foo(1), null, Foo(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.

Read/Writing Other Things

Apart from reading & writing java.lang.Strings, allows you to easily read from alternate sources such as CharSequences, Array[Byte]s, and java.nio.file.Paths:

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")
read[Thing](Files.newByteChannel(f)) ==> 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 this can be streamed directly to files or over the network without having to accumulate the serialized JSON in memory.


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.

Supported Types

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

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 22 fields.

Case classes are serialized using the apply and unapply methods on their companion objects. This means that you can make your own classes serializable by giving them companions apply and unapply. sealed hierarchies are serialized as tagged unions: whatever the serialization of the actual object, together with the fully-qualified name of its class, 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

Common Operations

The following common operations are available on any uPickle module, e.g. upickle.default or upickle.legacy:

def read[T: Reader](s: Transformable): T = s.transform(reader[T])

def readJs[T: Reader](s: Js.Value): T = s.transform(reader[T])

def reader[T: Reader] = implicitly[Reader[T]]

def write[T: Writer](t: T, indent: Int = -1): String = {

def writeJs[T: Writer](t: T): Js.Value = transform(t).to[Js.Value]

def writeTo[T: Writer](t: T, out:, indent: Int = -1): Unit = {
  transform(t).to(new Renderer(out, indent = indent))

def writer[T: Writer] = implicitly[Writer[T]]

def writable[T: Writer](t: T): Transformable = Transformable.fromTransformer(t, writer[T])

def readwriter[T: ReadWriter] = implicitly[ReadWriter[T]]

case class transform[T: Writer](t: T) extends Transformable{
  def transform[V](f: ujson.Visitor[_, V]): V = writer[T].transform(t, f)
  def to[V](f: ujson.Visitor[_, V]): V = transform(f)
  def to[V](implicit f: Reader[V]): V = transform(f)


Custom Picklers

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 .bimaping to doesn't need to be a case class, or be pickleable in any way, as long as the type you are .bimaping from is pickleable. The following example demonstrates using .bimap to define a serializer for non-case Scala class

import upickle.Js
class CustomThing2(val i: Int, val s: String)
object CustomThing2 {
  implicit val rw = upickle.default.readwriter[String].bimap[CustomThing2](
    x => 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] =
    x => Js.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")

Custom Keys

uPickle allows you to specify the key that a field is serialized with via a @key annotation

case class KeyBar(@upickle.key("hehehe") kekeke: Int)
object KeyBar{
  implicit def 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 def rw: RW[A] = RW.merge(, macroRW[C.type])
@upickle.key("Bee") case class B(i: Int) extends A
object B{
  implicit def 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:

Custom Configuration

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. 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.split("(?=[A-Z])", -1).map(_.toLowerCase).mkString("_")
  def snakeToCamel(s: String) = {
    val res = s.split("_", -1).map(x => x(0).toUpper + x.drop(1)).mkString
    s(0).toLower + res.drop(1)

  override def objectAttributeKeyReadMap(s: CharSequence) =
  override def objectAttributeKeyWriteMap(s: CharSequence) =

  override def objectTypeKeyReadMap(s: CharSequence) =
  override def objectTypeKeyWriteMap(s: CharSequence) =

// Default read-writing
upickle.default.write(Thing(1, "gg")) ==>
  """{"myFieldA":1,"myFieldB":"gg"}"""[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"}"""[Thing]("""{"my_field_a":1,"my_field_b":"gg"}""") ==>
  Thing(1, "gg")

If you are using uPickle to convert JSON from another source into Scala data structures, you can also configure it to map Option[T]s to nulls when the option is None:

object OptionPickler extends upickle.AttributeTagged {
  override implicit def OptionWriter[T: Writer]: Writer[Option[T]] =
    implicitly[Writer[T]].comap[Option[T]] {
      case None => null.asInstanceOf[T]
      case Some(x) => x

  override implicit def OptionReader[T: Reader]: Reader[Option[T]] =
      case null => None
      case x => Some(x)

This custom configuration allows you to treat nulls as Nones and anything else as Some(...)s. Simply import OptionPickler._ instead of the normal uPickle import throughout your project and you'll have the customized reading/writing available to you.


uPickle doesn't currently support:

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

Manual Sealed Trait 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.6.5"


You can use ujson to conveniently construct JSON blobs, either programmatically:

import ujson.Js
val json = Js.Arr(
  Js.Obj("myFieldA" -> 1, "myFieldB" -> "g"),
  Js.Obj("myFieldA" -> 2, "myFieldB" -> "k")

Or parsing them from strings, byte arrays or files:

val str = """[{"myFieldA":1,"myFieldB":"g"},{"myFieldA":2,"myFieldB":"k"}]"""
val json =
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.Js =

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.Js =

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

val data = Seq(Thing(1, "g"), Thing(2, "k"))
val json = upickle.default.writeJs(data)

json(0)("myFieldA") = 1337[Seq[Thing]](json)   ==> Seq(Thing(1337, "g"))
upickle.default.readJs[Seq[Thing]](json) ==> Seq(Thing(1337, "g"))

JSON Utilities

uJson comes with some convenient utilities on the `ujson` package:

def transform[T](t: Transformable, v: Visitor[_, T]) = t.transform(v)

def read(s: Transformable): Js.Value = transform(s, Js)

def copy(t: Js.Value): Js.Value = transform(t, Js)

def write(t: Js.Value, indent: Int = -1): String = {
  transform(t, StringRenderer(indent)).toString

def writeTo(t: Js.Value, out:, indent: Int = -1): Unit = {
  transform(t, Renderer(out, indent))

def validate(s: Transformable): Unit = transform(s, NoOpVisitor)

def reformat(s: Transformable, indent: Int = -1): String = {
  transform(s, StringRenderer(indent)).toString

def reformatTo(s: Transformable, out:, indent: Int = -1): Unit = {
  transform(s, Renderer(out, indent)).toString


uJson allows you seamlessly convert between any of the following forms you may find your JSON in:

This is done using the ujson.transform(source, dest) function:

// It can be used for parsing JSON into an AST
val exampleAst = Js.Arr(Js.Num(1), Js.Num(2), Js.Num(3))

ujson.transform("[1, 2, 3]", Js) ==> exampleAst

// Rendering the AST to a string
ujson.transform(exampleAst, StringRenderer()).toString ==> "[1,2,3]"

// Or to a byte array
ujson.transform(exampleAst, BytesRenderer()).toBytes ==> "[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

// `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:

ujson.transform("[1, 2, 3]", NoOpVisitor)

  ujson.transform("[1, 2, 3", NoOpVisitor)
  ujson.transform("[1, 2, 3]]", NoOpVisitor)

The normal methods to serialize Scala data-types is just shorthand for ujson.transform, using a upickle.default.writable(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.writable(Seq(1, 2, 3)), StringRenderer()).toString ==>

Other ASTs

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 st.ring


Maven Coordinates
libraryDependencies += "com.lihaoyi" %% "ujson-argonaut" % "0.6.5"
import ujson.argonaut.ArgonautJson
val argJson: argonaut.Json = ArgonautJson(
  """["hello", "world"]"""

val updatedArgJson = argJson.withArray(

val items: Seq[String] = ArgonautJson.transform(

items ==> Seq("HELLO", "WORLD")

val rewritten = upickle.default.transform(items).to(ArgonautJson)

val stringified = ArgonautJson.transform(rewritten, StringRenderer()).toString

stringified ==> """["HELLO","WORLD"]"""


Maven Coordinates
libraryDependencies += "com.lihaoyi" %% "ujson-circe" % "0.6.5"
import ujson.circe.CirceJson
val circeJson: io.circe.Json = CirceJson(
  """["hello", "world"]"""

val updatedCirceJson =
  circeJson.mapArray( => x.mapString(_.toUpperCase)))

val items: Seq[String] = CirceJson.transform(

items ==> Seq("HELLO", "WORLD")

val rewritten = upickle.default.transform(items).to(CirceJson)

val stringified = CirceJson.transform(rewritten, StringRenderer()).toString

stringified ==> """["HELLO","WORLD"]"""


Maven Coordinates
libraryDependencies += "com.lihaoyi" %% "ujson-play" % "0.6.5"
import play.api.libs.json._
val playJson: play.api.libs.json.JsValue = PlayJson(
  """["hello", "world"]"""

val updatedPlayJson = JsArray(
  for(v <-[JsArray].value)
    yield JsString([String].toUpperCase())

val items: Seq[String] = PlayJson.transform(

items ==> Seq("HELLO", "WORLD")

val rewritten = upickle.default.transform(items).to(PlayJson)

val stringified = PlayJson.transform(rewritten, StringRenderer()).toString

stringified ==> """["HELLO","WORLD"]"""


Maven Coordinates
libraryDependencies += "com.lihaoyi" %% "ujson-json4s" % "0.6.5"
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(

items ==> Seq("HELLO", "WORLD")

val rewritten = upickle.default.transform(items).to(Json4sJson)

val stringified = Json4sJson.transform(rewritten, StringRenderer()).toString

stringified ==> """["HELLO","WORLD"]"""

Cross-Library Conversions

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:

import ujson.circe.CirceJson
val circeJson: io.circe.Json = CirceJson(
  """["hello", "world"]"""

val updatedCirceJson =
  circeJson.mapArray( => x.mapString(_.toUpperCase)))

import play.api.libs.json._

val playJson: play.api.libs.json.JsValue = CirceJson.transform(

val updatedPlayJson = JsArray(
  for(v <-[JsArray].value)
    yield JsString([String].reverse)

val stringified = PlayJson.transform(updatedPlayJson, StringRenderer()).toString

stringified ==> """["OLLEH","DLROW"]"""


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.

JVM Case Class Serialization Performance

uPickle runs 30-50% faster than Circe for reads/writes, and ~200% faster than play-json.

Library Reads Writes Reads (Cached) Write (Cached)
Jackson-Scala 2,038,770 11,324,495
Play Json 987,940 1,357,490 1,122,132 1,574,340
Circe 2,360,411 2,139,692 2,732,585 2,172,389
upickle.default 3,135,576 3,496,939 3,706,736 4,392,758
upickle.legacy 2,315,402 2,526,501 3,889,198 4,285,537

As you can see, uPickle is pretty consistently ~30% faster than Circe for reads and ~50% faster for writes. It is also faster than Jackson-Scala on reads by a similar margin, although somehow Jackson-Scala has write performance that far outstrips everyone else. uPickle is 100-200% faster than Play-Json, depending on workload

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.

JVM AST Serialization Performance

If you benchmark the various libraries parsing from String -> AST, instead of String -> CaseClass, uPickle's numbers are in line with everyone else's:

Library AST Reads AST Writes
uPickle 2,890,396 4,055,356
Play Json 2,946,590 3,224,360
Circe 5,910,122 6,186,478
Argonaut 2,882,782 3,271,907
Json4s Native 3,214,664 1,239,398

One thing to note is that uPickle's AST-serialization performance is significantly worse than uPickle's CaseClass-serialization performance, for both reads (2,890,396 vs 3,706,736) and writes (4,392,758 vs 4,055,356) when the serializers are cached. This may seem surprising, but it makes sense when you realize that constructing a tree of dictionaries/arraybuffers (which is what an AST is) should be slower than constructing a tree of plain-old-java-objects.

JS Case Class Serialization Performance

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 117,181 194,582 138,665 253,164
Circe 132,519 441,906 151,030 644,901
upickle.default 613,727 1,041,798 862,280 1,548,753
upickle.legacy 541,242 913,455 874,563 1,469,347
upickle.default.web 518,840 952,393 912,521 1,731,286
upickle.legacy.web 500,202 872,095 782,801 1,568,826

Apart from the huge difference between uPickle and it's alternatives, this Scala.js/Node.js benchmark also shows a much larger effect for caching the picklers: 80-100% speedup. It seems likely that Scala.js/Node.js just isn't as good at optimizing away the overhead of temporary/throwaway data-structures, whether they're intermediate-ASTs or temporary-pickler-instances.

Another thing of note is the .web entries; these are benchmarking uPickle using the Node.js JSON.parse and JSON.stringify builtin functions, rather than our own parser. It turns out that these builtins appear to be slower than our parser for this benchmark, but they're available as the functions or the upickle.WebJson object if you for some reason want to use them.

uJson is a fork of Erik Osheim's excellent [Jawn]( JSON library, and inherits a lot of it's performance from Erik's work.

Version History