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.
For this demonstration project, I'm going to use a pair of basic classes.
The classes are fairly simple, but they do offer some small challenges for serialisation
- Read-only properties
- Parent references
- Special values - child collections that are only initialised when they are accessed and should be ignored if null or empty
Using YamlDotNet, you can serialise an object graph quite simply enough
Deserialising a YAML document into a .NET object is also quite straightforward
The following is an example of the YAML produced by the above classes with default serialisation
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
HasTopics properties were not serialised - although
YamlDotNet is ignoring the
BrowsableAttribute, it is
DefaultValueAttribute and skipping values which
are considered default, which is another nice feature.
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.
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
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.
If you specify this attribute on one property to set an order you'll most likely need to set it on all.
Unfortunately, while I could make use of the
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
Due to this requirement, I'm going to need to write some custom
serialisation code - enter the
To create a custom converter for use with YamlDotNet, we start
by creating a new class and implementing
First thing is to specify what types our class can handle via
In this case, we only care about our
ContentCategory class so
true for this type and
false for anything else.
Next, it's time to write the YAML content via the
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
type parameters. In my example, I can ignore
type though as I'm only supporting the one type.
IEmitter interface (accessed via the
is similar in principle to JSON.net's
except it is less developer friendly. Rather than having a
Write* methods or overloads similar to BCL
serialisation classes, it has a single
Emit method which takes
in a variety of objects.
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
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
Although the YAML specification allows for null values, attempting to emit a
Scalarwith 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).
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
SequenceEnd classes to tell YamlDotNet
we're going to serialise a list of values.
Topics property, the values are simple strings so we
can just emit a
Scalar for each entry in the list.
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
method of our custom type converter to throw an exception
instead of actually doing anything.
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
Serializer class. By calling the builder
WithTypeConverter method, we can enable the use of our
See the attached demonstration program for a fully working sample.
- 2017-04-01 - First published
- 2020-11-22 - Updated formatting
Like what you're reading? Perhaps you like to buy us a coffee?
- Using custom type converters with C# and YamlDotNet, part 2
- Using custom type converters with C# and YamlDotNet, part 1