Skip to content
senritsu edited this page Jul 14, 2014 · 2 revisions

Versioning in Full Serializer

Motivation

Loss of data is terrible and should be prevented, especially in games where designers can spend days tweaking a value. Luckily, Full Serializer has first-class support for data migrations.

Example

Let's say we're writing a game and we want to serialize the attributes of an item. Here's one example.

[fsObject("v1")] // shortcut for [fsObject(VersionString="v1", PreviousModels=new Type[] {})]
public class ItemAttributesModel_v1 {
    public int HealthBoost;
    public int ManaBoost;
}

We model serialized state of the item attributes using ItemAttributesModel_v1.

However, later on the design team decides that the mana boost should really be a random value between a range. Here's our updated attribute model:

[fsObject("v2", typeof(ItemAttributesModel_v1)]
public class ItemAttributesModel_v2 {
    public int HealthBoost;
    public int MinManaBoost;
    public int MaxManaBoost;

    // Migrate from the old v1 model. By using a constructor, we can do
    // whatever we want for the data migration.
    //
    // Note that the constructor does not need to be public (depending on
    // platform), so you can keep your public API clean.
    private ItemAttributesModel_v2(ItemAttributesModel_v1 v1) {
        HealthBoost = v1.HealthBoost;

        // We have to migrate a number into a range. We just take the minimum
        // as that number and the range as 10% of it.
        MinManaBoost = v1.ManaBoost;
        MaxManaBoost = (int)(v1.ManaBoost * 1.1);
    }
}

Note that we do not remove ItemAttributesModel_v1 from code. Full Serializer cannot recover the serialization model without it (enabling that would significantly bloat the emitted JSON).

Full Serializer makes this potentially complex migration easy; by using constructors for data migrations, we get an easy-to-use but extremely flexible versioning system.

If you introduce a v3 model, then you just migrate from the v2 model. If Full Serializer detects a v1 format, it will deserialize it as a v1 instance, run the v1 to v2 constructor, and then run the v2 to v3 constructor. Continuing the prior example, here is v3 of the model where Mana is renamed to Power:

[fsObject("v3", typeof(ItemAttributesModel_v2))]
public class ItemAttributesModel_v3 {
    public int HealthBoost;
    public int MinPowerBoost;
    public int MaxPowerBoost;

    private ItemAttributesModel_v3(ItemAttributesModel_v2 v2) {
        HealthBoost = v2.HealthBoost;
        MinPowerBoost = v2.MinManaBoost;
        MaxPowerBoost = v2.MaxManaBoost;
    }
}

We can serialize an instance of ItemAttributesModel_v1 and then deserialize it as an instance of ItemAttributesModel_v3, and it all works as expected.

Serialization Format

Adding versioning metadata to a model adds a small amount of information to the serialized state. For example, if we have this model:

[fsObject("Version Name")]
public class Model {
    public int Health;
}

Then Full Serializer will serialize new Model { Health = 3 } as

{
    "$version": "Version Name",
    "Health": 3
}

If versioning were not enabled for Model, then the emitted JSON would be simply:

{
    "Health": 3
}

As always, versioning just works, even when you combine it with inheritance, cyclic object graphs, and other tricky serialization features.

Advanced Features

Multiple Prior Models

The versioning engine supports having multiple prior models. For example,

[fsObject("v1_a")]
public class Model_v1_a {
    public int A;
}
[fsObject("v1_b")]
public class Model_v1_b {
    public int B;
}

and then we can combine these two models into Model_v2 as follows:

[fsObject("v2", typeof(Model_v1_a), typeof(Model_v1_b))]
public class Model_v2 {
    public int A;
    public int B;

    private Model_v2(Model_v1_a v1) {
        A = v1.A;
    }
    private Model_v2(Model_v1_b v2) {
        B = v2.B;
    }
}