Home

Awesome

Prickle

Prickle is a library for easily pickling (serializing) object graphs between Scala and Scala.js.

It is based upon scala-js-pickling, but adds several improvements & refinements:

Currently, prickle supports automatic, reflection-free recursive pickling of

Getting Prickle

Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.benhutchison/prickle_2.13)

Scala.jvm 2.13 "com.github.benhutchison" %% "prickle" % "<version above>"

Scala.js 1.x on 2.13 "com.github.benhutchison" %%% "prickle" % "<version above>"

To use, import the package, but do not import on the Pickler & Unpickler objects

import prickle._

//!Don't do this. Not Necessary
import Pickler._

Prickle depends upon microjson in its default pickle configuration.

Prickle is open source under the Apache 2 license.

Runnable Example

To run:

> example/run

The first example demonstrates:

import prickle._

sealed trait Fruit
case class Apple(isJuicy: Boolean) extends Fruit
case class Lemon(sourness: Double) extends Fruit
case class FruitSalad(components: Seq[Fruit]) extends Fruit
case object TheDurian extends Fruit

object Example extends App {

  println("\n1. No preparation is needed to pickle or unpickle values whose static type is exactly known:")

  val apples = Seq(Apple(true), Apple(false))
  val pickledApples = Pickle.intoString(apples)
  val rehydratedApples = Unpickle[Seq[Apple]].fromString(pickledApples)


  println(s"A bunch of Apples: ${apples}")
  println(s"Pickled apples: ${pickledApples}")
  println(s"Rehydrated apples: ${rehydratedApples}\n")


  println("2. To pickle a class hierarchy (aka 'Sum Type'), create a CompositePickler and enumerate the concrete types")

  //implict defs/vals should have an explicitly declared type to work properly
  implicit val fruitPickler: PicklerPair[Fruit] = CompositePickler[Fruit].
    concreteType[Apple].concreteType[Lemon].concreteType[FruitSalad].concreteType[TheDurian.type]

  val sourLemon = Lemon(sourness = 100.0)
  //fruitSalad's concrete type has been forgotten, replaced by more general supertype
  val fruitSalad: Fruit = FruitSalad(Seq(Apple(true), sourLemon, sourLemon, TheDurian))

  val fruitPickles = Pickle.intoString(fruitSalad)

  val rehydratedSalad = Unpickle[Fruit].fromString(fruitPickles)

  println(s"Notice how the fruit salad has multiple references to the same object 'sourLemon':\n${fruitSalad}")

  println(s"In the JSON, 2nd and subsequent occurences of an object are replaced by refs:\n${fruitPickles}")

  println(s"The rehydrated object graph doesnt contain duplicated lemons:\n${rehydratedSalad}\n")
}

Changelog

VersionWhenChanges
1.0.3Oct 14SortedMap support. Duration Support. 2.10.x binary added
1.1.0Nov 14Collection picklers support shared objects properly. Iterable support.
1.1.1Jan 15Example showing how to handle static reference data in a pickled object graph.
1.1.2Jan 15Fix #9 Double serialization bug
1.1.3Feb 152.10.x support dropped, scala-js-0.6.0 support added
1.1.4Mar 15List and immutable Seq support, upgrade to Scala.js 0.6.1
1.1.5Apr 15Performance improvement: use object identity for equality check during unpickle, using mutable Builder during Map unpickle
1.1.6Jun 15Performance: use builder for Seq/Iterable collections. UUID support. Upgrade scalajs 0.6.3
1.1.7Jun 15Performance: microjson 1.3 removed some silly inefficiency. Support Unit picklers
1.1.8Jul 15Support Vector picklers
1.1.9Aug 15Expose some Pickler/Unpickler helper methods for use by custom picklers
1.1.10Nov 15Fix #28: account for unstable Set iteration order
1.1.11Jul 16Fix incorrect utest runtime dependency, freshen libraries
1.1.12Oct 16Fix #34: fix custom pickle format prefix. Upgrade to scalajs 0.6.12
1.1.13Nov 16Scala 2.12, update libs, cleanup sbt files
1.1.14Feb 17Fix #37: better support for fields that are aliased types
1.1.15Apr 20Bad release
1.1.16Apr 20Scala 2.13 only due to cross-compile difficulties in Scala collections redesign, Scalajs 1.x

Pickling to String by Default

Prickle expects you probably want to pickle to and from a json String, so this is the default. Because Strings are defined in both Scala and Scala.js core lib, there is no need to depend upon a platform-specific json dependency.

Call prickle.Pickle.intoString() to pickle your object. The static type of the passed object will be used to search for Pickler typeclasses in implicit scope. If none are found, and the object is a case-class or case-object, a macro will materialize a pickler using compile-time reflection to analyze the fields of the object.

When Unpickling with prickle.Unpickle[T].fromString(), you must tell prickle what type to unpickle into, since it's unable to determine this from the String parameter.

val p = Person("Ben", "Hutchison")

val s: String = Pickle.intoString(p)

val tryPerson = Unpickle[Person].fromString(s)

Under the hood, prikle converts objects to/from a json model (microjson.JsValue) based on the microjson library. MicroJson then renders or parses the JSON object graph to a flat String.

Support for Class Hierarchies and Sum Types

It's common to have a hierarchy of classes where the concrete type of a value is not known statically. In some contexts these are called Sum Types.

Prickle supports these via CompositePicklers. These are not automically derived by a macro, but must be configured by the programmer, and assigned to an implicit val.

Example: How to creates a PicklerPair[Fruit], that handles two cases of fruit, Apples and Lemons:

import prickle._ 

implicit val fruitPickler = CompositePickler[Fruit].concreteType[Apple].concreteType[Lemon]

val fruit1: Fruit = new Apple(true)

val jsonString = Pickle.intoString(apple)

The pickle and unpickle operations can be specified together, yielding a PicklerPair[A], that knows how to pickle/unpickle values of type A, and all specified concrete subclasses. There are background implicit conversions in the Pickler and Unpickler that can auto-unpack PicklerPairs into their two parts.

(Note also the detail that fruit1 is declared to have super-type Fruit. Problems would result if this was omitted, as the val fruit1 would then have inferred subtype Apple. In that case, the compiler will prefer to auto-generate a Pickler[Apple] via macro rather than use the Pickler[Fruit].)

Improved Type-Safety vs scala-js-pickling

CompositePicklers play a similar role to the PicklerRegistry used in scala-js-pickling, but are safer. In Prickle, missing Picklers will normally result in a compile-time error, as an implicit not found. (The exception is unregistered concrete subclasses of a CompositePickler.) However, in Scala-js-pickling, the discovery of missing un/picklers occurs at runtime when un/pickling is attempted.

Composite Picklers Vs Singleton Registry

Picklers, Unpicklers, Macros and Formats

Like scala-pickling and scala-js-pickling, prickle uses implicit Pickler[T] and Unpickler[T] type-classes to transform values of type T.

For case classes and case objects, these type classes will be automatically materialized via an implicit macro, if they aren't found in implicit scope. This is recursive, so picklers & unpicklers for each field will also be resolved and possibly materialized.

If there is a non-case class you wish to pickle that's unsupported out-of-the-box, you can define your own and put them in implicit scope. See prickle.Pickler and prickle.Unpickler for examples.

Prickle isn't limited to pickling to Strings - any json-like format can be used. You will need to implementprickle.PReader and prickle.PBuilder for your format, and pass a custom PConfig when un/pickling.

import prickle._

implicit val myConfig: PConfig[Array[Byte]] = ??? //..your defn goes here

val p = Person("Ben", "Hutchison")

val bytes: Array[Byte] = Pickle(p)

val tryPerson = Unpickle[Person].from(bytes)

Unpickling yields a Try

It's tempting to think of Pickling and Unpickling as symmetrical, inverse operations (eg T => String, String => T) but there's a difference: Unpickling is far more likely to fail.

This stems from the nature of the operations. Pickling transforms a structured, well typed object graph into flattened, stringly-typed data. Unpickling takes a weakly-typed string and re-constructs the typed object-graph from it. Most arbitrary strings won't re-construct valid object graphs, so the unpickle operation attempts to move from a higher entropy state to a lower entropy state.

Prickle acknowledges the possibility of failure by returning a Try[T] when attempting to unpickle (An extended talk about the philosophy guiding this design, with supa-crunch audio)

Support for Shared objects

What is meant by "shared" here are objects that are referenced more than once in an object graph. To avoid duplicating such objects when pickling, prickle's algorithm remembers what objects it has pickled so far, and introduce references to already pickled state when it re-encounters them. On the unpickle side, prickle tracks an IDs associated with each unpickled object and resolved references to IDs it has already encountered in the stream.

Shared objects brings a memory and pickled-data overhead, since a mapping between objects and IDs must be maintained during pickling and unpickling. It can be turned off in the PConfig by setting isCyclesSupported = false.

Note on terminology: sometimes object graphs with shared objects are described as having circular or cyclic references, but there is a subtle difference. Circular references implies there is a path through the graph that returns to the originating object. Shared references is a weaker condition, that simply implies there are two different paths to the same object. The former cannot result from the use of purely immutable data, but shared objects certainly can- and does- often.

Supporting Static Reference Data

The second AdvancedLookupExample shows how prickle can be extended to handle reference data in the pickled object graph. Here, reference data denotes pre-existing values presumed to exist the static environment on both the pickle and unpickle sides. When an object graph refers to such values, typically it is not desirable to pickle the actual data, but just a reference to it, and then to re-enstate the reference on the other side by looking it up from an Id.

Controlling Pickling via PConfig

The pickle and unpickle operations take an implicit PConfig ("pickle-config") which specifies:

These internal keys are

##Limitations

Troubleshooting

If you escape "Implicit Parameter Not Found" errors when you first use prickle on a non-trivial problem, you're very lucky! For the rest of you, here's some tips for diagnosing such problems:

implicit val personPickler: Pickler[Person] = Pickler.materializePickler[Person]
import prickle._
trait Fruit
class Lemon extends Fruit
implicit val fruitPickler: Pickler[Fruit] = ???
val l = new Lemon()

//won't compile, because we don't have a Pickler of *Lemons*
//Pickle(l)

//Acribing type Fruit (up-casting) compiles OK
Pickle(l: Fruit)

Contributors

Prickle is written and maintained by Ben Hutchison.

Credit & thanks for prior work to Sebastien Doeraene for scala-js-pickling, Li Haoyi for microjson

Contributors: @xeno-by, @antonkulaga, @ddispaltro, @mysticfall

YourKit is kindly supporting this open source project with its full-featured Java Profiler.