logo

Serialization

Overview

Serialization is the process of writing objects to an output stream, and reading them back from an input stream. Serialization provides a simple mechanism to persist objects to a file or to pass objects over a network. Serialization is also used with threading as a safe way to pass messages between threads. Fan serialization uses a human friendly text format which looks a lot just like Fan source code (in fact it is a subset of the source grammar).

Data Model

Serialization in languages such as Java or C# are graph based - they will handle an arbitrary number of references to a particular object. Fan serialization is strictly tree based, it will not attempt to keep track of object references - it is up to you design your data models as a tree. If you need to cross reference objects in your tree, then you should use a Uri or some other identifier.

Each object in the tree is classified as a literal, simple, or complex. Most of the standard Fan literals such as Bool, Int, Str are supported as well as the collections types List and Map. Simples are leaf nodes serialized via a string representation. Complexes are an aggregate node defined by their fields which store nested objects (either literals, simples, or other complexes).

Serializable

The serializable facet is used to mark types which are serialized as a complex. By default a serializable object will serialize all of its non-const fields. You can use the transient facet to annotate a field which should not be serialized. A contrived example:

@serializable
class Rectangle
{
  Int x; Int y
  Int w; Int h
  @transient Int area
}

Simple

The simple facet is used to mark types which are serialized atomically to and from a string representation. The sys::Obj.toStr method must return a suitable string representation of the object. Each simple type must also declare a static method called fromStr which takes one or more parameters where the first parameter is a Str and returns an instance of the object. An example:

@simple
class Point
{
  static Point fromStr(Str s) { t := s.split(","); return make(t[0].toInt, t[1].toInt) }
  new make(Int x, Int y) { this.x = x; this.y = y }
  override Str toStr() { return "$x,$y" }
  Int x := 0
  Int y := 0
}

Streams

Serializing objects to and from streams is a piece of cake:

// write an object to an output stream
out.writeObj(obj)

// read an object from an input stream
obj := in.readObj

Both Buf and File have convenience methods. For example to serialize to and from a file:

obj := [true, 5, "hi", `file.txt`]
f := File.make(`test.txt`)
f.writeObj(obj)
obj2 := f.readObj

By default writeObj will optimize for performance. But if you are generating a file which should look pretty for humans to read and edit, you can control the output using options. For example to indent each level of the output tree by 2 spaces and skip fields at their default values:

out.writeObj(obj, ["indent":2, "skipDefaults":true])

Syntax

The Fan serialization syntax is designed to be easy to read and write by a human, but also efficient for machine processing. The syntax is based on the Fan programming language itself, although it is purely declarative (no expressions or behavior). An object is defined as one of:

  • Literal: one of the standard Fan literals using the exact same representation as you would use in your source code (this includes List and Map)
  • Simple: the string representation of a simple type
  • Complex: a type and its list of field name/values pairs

The Fan programming language is a complete superset of the serialization syntax - you can paste any serialized object into a source file as an expression.

Literals

Most of the standard Fan literals are serialized using the same representation as defined by the Fan programming language:

NOTE: the special Float values NaN, INF, and -INF must be represented using the simple syntax: sys::Float("NaN") sys::Float("INF") sys::Float("-INF")

Simples

A simple is serialized as: <type>("<toStr>"). When writing the object, the Obj.toStr method is called to obtain the string representation. When reading the object the static fromStr method is used to decode the string back into an object. Examples:

sys::Version("1.2")
sys::Depend("foo 1.2-3.4")

You may use this syntax directly in source code via the simple expression.

Complex

A complex is serialized as a list of field name/value pairs separated by a newline or a semicolon (just like Fan statements). Any field can be omitted, in which case the field's default is used. The syntax for a complex is:

<type>
{
  <field1> = <value1>
  <field2> = <value2>
  ...
}

An example of a serializable class and an serialized instance:

@serializable class Person
{
  Str name
  Int age
  Str[] children
  Str address
}

acme::Person
{
  name = "Homer Simson"
  age = 39
  children = ["Bart", "Lisa", "Maggie"]
}

You may use this syntax directly in source code via with blocks.

Grammar

The formal grammar of the Fan serialization format:

obj     := literal | simple | complex
literal := bool | int | float | str | duration | uri | typeLiteral | list | map
simple  := type "(" str ")"
complex := type "{" [fields] "}"
fields  := field (eos field)*
eos     := ";" | newline
field   := id "=" obj

The literal and type productions use the same grammar as the Fan programming language. However the type production can never be a method type.

NOTE: currently we don't support the using directive in serialization, in which case all type productions must be fully qualified names.