
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
andMap
) - 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.