One of our internal tools eschews XML or JSON configuration files in favour of something more human readable - YAML using YamlDotNet. For the most part the serialisation and deserialisation of YAML documents in .NET objects is as straight forward as using libraries such as JSON.net but when I was working on some basic serialisation there were a few issues.
A demonstration program showing the basics of YAML serialisation
Setting the scene
For this demonstration project, I'm going to use a pair of basic classes.
For a format that is "human friendly" this is quite verbose with a lot of extra clutter as the serialisation has included the read-only properties (which will then cause a crash on deserialisation), and our create-on-demand collections are being created and serialised as empty values. It is also slightly alien when you consider the alias references. While those are undeniably cool (especially as YamlDotNet will recreate the references), the nested nature of the properties implicitly indicate the relationships and are therefore superfluous in this case
It's also worth pointing out that the order of the serialised values matches the ordering in code file - I always format my code files to order members alphabetically, so the properties are also serialised alphabetically.
You can also see that, for the most part, the HasCategories and HasTopics properties were not serialised - although YamlDotNet is ignoring the BrowsableAttribute, it is processing the DefaultValueAttribute and skipping values which are considered default, which is another nice feature.
Resolving some issues
Similar to Json.NET, you can decorate your classes with attributes to help control serialisation, and so we'll investigate these first to see if they can resolve our problems simply and easily.
Excluding read-only properties
The YamlIgnoreAttribute class can be used to force certain properties to be skipped, so applying this attribute to properties with only getters is a good idea.
We can control the order in which YamlDotNet serialises using the YamlMemberAttribute. This attribute has various options, but for the time being I'm just looking at ordering - I'll revisit this attribute in the next post.
csharp
[YamlMember(Order =1)]publicstring Name {get;set;}
If you specify this attribute on one property to set an order you'll most likely need to set it on all.
Processing the collection properties
Unfortunately, while I could make use of the YamlIgnore and YamlMember attributes to control some of the serialisation, it wouldn't stop the empty collection nodes from being created and then serialised, which I didn't want. I suppose I could finally work out how to make DefaultValue apply to collection classes effectively, but then there wouldn't be much point in this article!
Due to this requirement, I'm going to need to write some custom serialisation code - enter the IYamlTypeConveter interface.
Creating a custom converter
To create a custom converter for use with YamlDotNet, we start by creating a new class and implementing IYamlTypeConverter.
csharp
internalsealedclass ContentCategoryYamlTypeConverter : IYamlTypeConverter
{publicbool Accepts(Type type){}publicobject ReadYaml(IParser parser, Type type){}publicvoid WriteYaml(IEmitter emitter,object value, Type type){}}
First thing is to specify what types our class can handle via the Accepts method.
csharp
privatestaticreadonly Type _contentCategoryNodeType =typeof(ContentCategory);publicbool Accepts(Type type){return type == _contentCategoryNodeType;}
In this case, we only care about our ContentCategory class so I return true for this type and false for anything else.
Next, it's time to write the YAML content via the WriteYaml method.
The documentation for YamlDotNet is a little lacking and I didn't find the serialisation support to be particularly intuitive, so the code I'm presenting below is what worked for me, but there may be better ways of doing it.
First we need to get the value to serialise - this is via the value and type parameters. In my example, I can ignore type though as I'm only supporting the one type.
csharp
publicvoid WriteYaml(IEmitter emitter,object value, Type type){
ContentCategory node;
node =(ContentCategory)value;}
The IEmitter interface (accessed via the emitter parameter) is similar in principle to JSON.net's JsonTextWriter class except it is less developer friendly. Rather than having a number of Write* methods or overloads similar to BCL serialisation classes, it has a single Emit method which takes in a variety of objects.
Writing property value maps
To create our dictionary map, we start by emitting a MappingStart object. Of course, if you have a start you need an end so we'll close by emitting MappingEnd.
csharp
emitter.Emit(new MappingStart(null,null,false, MappingStyle.Block));// reset of serialisation code
emitter.Emit(new MappingEnd());
YAML supports block and flow styles. Block is essentially one value per line, while flow is a more condensed comma separated style. Block is much more readable for complex objects, but flow is probably more valuable for short lists of simple values.
Next we need to write our key value pairs, which we do by emitting pairs of Scalar objects.
Although the YAML specification allows for null values, attempting to emit a Scalar with a null value seems to destabilise the emitter and it will promptly crash on subsequent calls to Emit. For this reason, in the code above I wrap each pair in a null check. (Not to mention if it is a null value there is probably no need to serialise anything anyway).
Writing lists
With the basic properties serialised, we can now turn to our child collections.
This time, after writing a single Scalar with the property name instead of writing another Scalar we use the SequenceStart and SequenceEnd classes to tell YamlDotNet we're going to serialise a list of values.
For our Topics property, the values are simple strings so we can just emit a Scalar for each entry in the list.
As the Categories property returns a collection of ContentCategory objects, we can simply start a new list as we did for topics and then recursively call WriteYaml to write each child category object in the list.
In this article, I'm only covering custom serialisation. However, the beauty of this code is that it doesn't generate different YAML from default serialisation, it only excludes values that it knows are defaults or that can't be read back, and provides custom ordering of values. This means you can use the basic deserialisation code presented at the start of this article and it will just work, as demonstrated by the sample program accompanying this post.
For this reason, for the time being I change the ReadYaml method of our custom type converter to throw an exception instead of actually doing anything.
csharp
publicobject ReadYaml(IParser parser, Type type){thrownew NotImplementedException();}
Using the custom type converter
Now we have a functioning type converter, we need to tell YamlDotNet about it.
At the start of the article, I showed how you create a SerializerBuilder object and call its Build method to get a configured Serializer class. By calling the builder objectsWithTypeConverter method, we can enable the use of our custom converter.
The founder of Cyotek, Richard enjoys creating new blog content for the site. Much more though, he likes to develop programs, and can often found writing reams of code. A long term gamer, he has aspirations in one day creating an epic video game - but until that time comes, he is mostly content with adding new bugs to WebCopy and the other Cyotek products.
Recently I discussed using type converters to perform custom serialization of types in YamlDotNet. In this post I'll concentrate on expanding the type converter to support deserialization as well.
One of our internal tools eschews XML or JSON configuration files in favour of something more human readable - YAML using YamlDotNet. For the most part the serialisation and deserialisation of YAML documents in .NET objects is as straight forward as using libraries such as JSON.net but when I was working on some basic serialisation there were a few issues. This article describes how to use the `IYamlTypeConverter` interface to handle custom YAML serialisation functionality.