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.
Setting the scene
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
Basic serialisation
Using YamlDotNet, you can serialise an object graph quite simply
enough
Basic deserialisation
Deserialising a YAML document into a .NET object is also quite
straightforward
Serialisation shortcomings
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 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.
Changing serialisation order
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.
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.
First thing is to specify what types our class can handle via
the Accepts method.
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.
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.
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.
Deserialisation
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.
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.
See the attached demonstration program for a fully working sample.
Update History
2017-04-01 - First published
2020-11-22 - Updated formatting
Like what you're reading? Perhaps you like to buy us a coffee?
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.