I'm trying to parse this YAML document using YamlDotNet:
title: Document with dynamic properties
/a:
description: something
/b:
description: other something
Into this object:
public class SimpleDoc
{
[YamlMember(Alias = "title")]
public string Title { get; set;}
public Dictionary<string, Path> Paths { get; set; }
}
public class Path
{
[YamlMember(Alias = "description")]
public string Description {get;set;}
}
I wish that the /a /b or any other unmatched properties end up inside the Paths dictionary. How can I configure YamlDotNet to support this scenario?
The deserialization setup is:
// file paths is the path to the specified doc :)
var deserializer = new Deserializer();
var doc = deserializer.Deserialize<SimpleDoc>(File.OpenText(filePath));
However it fails given that there's no property /a on SimpleDoc. And if I set up the setting ignoreUnmatched in the constructor to true it is ignored as expected.
I needed this for a project where the YAML schema is defined as JSON schema, which I used to generate classes (via
NJsonSchema
). But, the actual data comes as YAML. So, I need to deserialize the YAML into the classes.I exploit the mechanism where, if a class implements
IDictionary<string, object
orIDictionary<object, object>
, YamlDotNet will put all keys/values from that node into the dictionary. If I had derived the class fromDictionary<string, object>
, I wouldn't have access to modify how values are added to that dictionary, so I was forced to implement the interface.In this example,
Language
is the class I'm trying to deserialize. I have other properties on this class as part of my generated classes (via the JSON schema generator). Since they are partial, I can implement this workaround.Basically, I created
AddAndMap
and applied it to anywhere in the interface that adds dictionary entries. I also created theAdditionalProperties
dictionary to hold our values. Keep in mind that we need all the values in theIDictionary
for serializing this class properly.Next, I always add the entry to the backing
_dictionary
and then determine if I have a property to provide the value to. If I don't have a property that this value should map to, I put the value into theAdditionalProperties
dictionary instead.To determine if I have an available property for deserization, I had to write a few extension methods.
Main thing to note is that this only works on properties of the class that are primitives or
Dictionary<object, object>
. The reason is because it usesTypeConverter.ChangeType
to return the appropriate object when setting the property. If you made a way to interpret how custom classes would be created, you would just replace theTypeConverter.ChangeType
call with that solution.To get the deserializable properties of a class, I use reflection to get the
YamlMemberAttribute
of the properties on that class. Then, I put thePropertyInfo
of that property into a dictionary where the key is the value ofAlias
from theYamlMemberAttribute
. In my solution here, all properties require an alias to be defined for the property.Then, I find the property with an alias that matches the key that I got from the
AddAndMap
call. This recursively happens so that (in the future) it would be possible to have this work with custom class properties. It checks forDictionary<object, object>
as the value type becauseYamlDotNet
, when deserializing something into a dictionary, will always deserialize sub-classes intoDictionary<object, object>
.Overall
This solution works and could be simplified if you only have flat (no custom class properties) of the class you are trying to deserialize. Or, it could be extended to support custom class properties. The solution could also be made into a base type that your other classes derive from, and you'd do
GetType()
inDeserializableProperties
instead oftypeof(Language)
. It is an overly verbose solution, but untilYamlDotNet
has a cleaner/simpler/proper solution, this was the best I could come up with after exploring their source code.Lastly, keep in mind that because your class derives of
IDictionary
,YamlDotNet
will not serialize any properties from your class, even if they have theYamlMember
attributes on them. IfYamlDotNet
would provide that functionality, I had a cleaner solution in mind originally. Alas, that is not the case.