10.7 Serialization

Many runtime values can be serialized and deserialized using the haxe.Serializer and haxe.Unserializer classes. Both support two usages:

  1. Create an instance and continuously call the serialize/unserialize method to handle multiple values.
  2. Call their static run method to serialize/deserialize a single value.

The following example demonstrates the first usage:

import haxe.Serializer;
import haxe.Unserializer;

class Main {
  static function main() {
    var serializer = new Serializer();
    serializer.serialize("foo");
    serializer.serialize(12);
    var s = serializer.toString();
    trace(s); // y3:fooi12

    var unserializer = new Unserializer(s);
    trace(unserializer.unserialize()); // foo
    trace(unserializer.unserialize()); // 12
  }
}

The result of the serialization (here stored in local variable s) is a String and can be passed around at will, even remotely. Its format is described in Serialization format.

Supported values
Serialization configuration

Serialization can be configured in two ways. For both a static variable can be set to influence all haxe.Serializer instances, and a member variable can be set to only influence a specific instance:

  • USE_CACHE, useCache: If true, repeated objects are serialized by reference. This can avoid infinite loops for recursive data at the expense of longer serialization time. By default, this value is false.
  • USE_ENUM_INDEX, useEnumIndex: If true, enum constructors are serialized by their index instead of their name. This can make the serialization string shorter but breaks if enum constructors are inserted into the type before deserialization. By default, this value is false.
Deserialization behavior

If the serialization result is stored and later used for deserialization, care has to be taken to maintain compatibility when working with class and enum instances. It is then important to understand exactly how unserialization is implemented.

  • The type has to be available in the runtime where the deserialization is made. If dead code elimination is active, a type which is used only through serialization might be removed.
  • Each Unserializer has a member variable resolver which is used to resolve classes and enums by name. Upon creation of the Unserializer this is set to Unserializer.DEFAULT_RESOLVER. Both that and the instance member can be set to a custom resolver.
  • Classes are resolved by name using resolver.resolveClass(name). The instance is then created using Type.createEmptyInstance, which means that the class constructor is not called. Finally, the instance fields are set according to the serialized value.
  • Enums are resolved by name using resolver.resolveEnum(name). The enum instance is then created using Type.createEnum, using the serialized argument values if available. If the constructor arguments were changed since serialization, the result is unspecified.
Custom (de)serialization

If a class defines the member method hxSerialize, that method is called by the serializer and allows custom serialization of the class. Likewise, if a class defines the member method hxUnserialize it is called by the deserializer:

import haxe.Serializer;
import haxe.Unserializer;

class Main {

    var x:Int;
    var y:Int;

    static function main() {
      var s = Serializer.run(new Main(1, 2));
      var c:Main = Unserializer.run(s);
      trace(c.x); // 1
      trace(c.y); // -1
    }

    function new(x, y) {
      this.x = x;
      this.y = y;
    }

    @:keep
    function hxSerialize(s:Serializer) {
        s.serialize(x);
    }

    @:keep
    function hxUnserialize(u:Unserializer) {
        x = u.unserialize();
        y = -1;
    }
}

In this example we decide that we want to ignore the value of member variable y and do not serialize it. Instead we default it to -1 in hxUnserialize. Both methods are annotated with the :keep metadata to prevent dead code elimination from removing them as they are never properly referenced in the code.