-
Notifications
You must be signed in to change notification settings - Fork 170
Versioning
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.
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.
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.
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;
}
}