Serialize/Deserilize derived classes
federicomrc opened this issue · comments
It would be really useful to be able to serialize and deserialize derived classes.
The actual behaviour is the one listed below:
class A {
public int AA { get; set; }
}
class B : A {
public int BB { get; set; }
}
class C : A {
public int CC { get; set; }
}
class Test
{
public List<A> MyList { get; set; }
}
/* Inside some function */
var test = new Test()
{
MyList = new List<A>()
{
new A(){/*...*/},
new B(){/*...*/},
new C(){/*...*/},
}
};
// s correctly contains all properties for each element in MyList
// MyList = [
// { AA = 1 },
// { BB = 22, AA = 2 },
// { CC = 33, AA = 3 },
// ]
var s = TomletMain.TomlStringFrom(test);
// myDeserializedTest contains only properties contained in A, as if MyList
// contained only instances of A
// MyList = [
// { AA = 1 },
// { AA = 2 },
// { AA = 3 },
// ]
var myDeserializedTest = TomletMain.To<Test>(s);
I think it would be very useful if deserialization returned the correct types (or the subtypes "casted" to the base class), i.e.
// MyList = [
// { AA = 1 }, ----> Type A
// { BB = 22, AA = 2 }, ----> Type B
// { CC = 33, AA = 3 }, ----> Type C
// ]
var myDeserializedTest = TomletMain.To<Test>(s);
EDIT:
In the first sentence I meant "deserialize derived classes", not "deserialize base classes"
It's a bit tricky but it is somewhat possible to achieve this behavior manually, by storing a property for your base class that also persists the class name into TOML (eg an extra field TypeName = "B"). Then in the deserializer you can look up this class and use a bit of reflection find the derived class and call TomletMain.To() on that.
Creating user-defined classes at runtime is a bit of a security risk so make sure to limit what classes can be loaded.
An implementation might look something like this:
class A {
public string TypeName { get { return GetType().Name; } } // you could also manually define a TypeName property in A, B, and C, but this saves a bit of work
}
...
TomletMain.RegisterMapper<A>(
null,
tomlValue =>
{
var typeName = tomlTable.GetString("TypeName");
if (typeName == null) throw new Exception("Error loading A: TypeName not given"); // alternately you could assume the absence of a TypeName indicates it is the base class
var cls = allowableLoadedTypes.FirstOrDefault(cls => cls.Name == typeName);
if (cls == null) throw new Exception($"Error loading A: Not allowed to parse a {typeName}");
return (A) TomletMain.To(cls, tomlValue);
}
);
The only catch is that this method probably won't work if you have instances of AA and not just base classes, since the custom deserializer for A will end up calling TomletMain.To(A), which will call the deserializer again, and so on.
EDIT: it looks like TomlCompositeSerializer.For(A)/TomlCompositeDeserializer.For(A) could be used to avoid that problem by manually getting the ordinary serializer, however these classes are internal so they're not accessible to consumers of the library.
I can definitely post a release with those composite serializer/deserializer methods exposed via some sort of API, for this use case.