Virtlink Virtlink - 12 days ago 10
YAML Question

How to deserialize a YamlNode in YamlDotNet?

I have an application where I read arbitrary Yaml files whose structure I don't know in advance. I found the

YamlStream
and other
YamlNode
implementations useful, as they allow me to traverse the whole Yaml file. However, at some point I have a
YamlNode
, usually a
YamlScalarNode
, and I want to use YamlDotNet's ability to deserialize that node into an object. How can I do that?

This is what I've tried. It's ugly, and it works only for nodes with explicit tags (e.g.
!!bool "true"
becomes
true
, but
1
becomes
"1"
):

private T DeserializeNode<T>(YamlNode node)
{
if (node == null)
return default(T);

using (var stream = new MemoryStream())
using (var writer = new StreamWriter(stream))
using (var reader = new StreamReader(stream))
{
new YamlStream(new YamlDocument[] { new YamlDocument(node) }).Save(writer);
writer.Flush();
stream.Position = 0;
return new Deserializer().Deserialize<T>(reader);
}
}


There must be a better way that I just haven't found yet.

Answer

Currently there is no way to deserialize from a YamlNode, your approach is one of the possible workarounds. If you want to avoid writing the node to a buffer, you can implement the IParser interface that reads from a YamlNode, like this example.

The way I did it in the above example is to create an adapter that converts a YamlNode to an IRnumerable<ParsingEvent>:

public static class YamlNodeToEventStreamConverter
{
    public static IEnumerable<ParsingEvent> ConvertToEventStream(YamlStream stream)
    {
        yield return new StreamStart();
        foreach (var document in stream.Documents)
        {
            foreach (var evt in ConvertToEventStream(document))
            {
                yield return evt;
            }
        }
        yield return new StreamEnd();
    }

    public static IEnumerable<ParsingEvent> ConvertToEventStream(YamlDocument document)
    {
        yield return new DocumentStart();
        foreach (var evt in ConvertToEventStream(document.RootNode))
        {
            yield return evt;
        }
        yield return new DocumentEnd(false);
    }

    public static IEnumerable<ParsingEvent> ConvertToEventStream(YamlNode node)
    {
        var scalar = node as YamlScalarNode;
        if (scalar != null)
        {
            return ConvertToEventStream(scalar);
        }

        var sequence = node as YamlSequenceNode;
        if (sequence != null)
        {
            return ConvertToEventStream(sequence);
        }

        var mapping = node as YamlMappingNode;
        if (mapping != null)
        {
            return ConvertToEventStream(mapping);
        }

        throw new NotSupportedException(string.Format("Unsupported node type: {0}", node.GetType().Name));
    }

    private static IEnumerable<ParsingEvent> ConvertToEventStream(YamlScalarNode scalar)
    {
        yield return new Scalar(scalar.Anchor, scalar.Tag, scalar.Value, scalar.Style, false, false);
    }

    private static IEnumerable<ParsingEvent> ConvertToEventStream(YamlSequenceNode sequence)
    {
        yield return new SequenceStart(sequence.Anchor, sequence.Tag, false, sequence.Style);
        foreach (var node in sequence.Children)
        {
            foreach (var evt in ConvertToEventStream(node))
            {
                yield return evt;
            }
        }
        yield return new SequenceEnd();
    }

    private static IEnumerable<ParsingEvent> ConvertToEventStream(YamlMappingNode mapping)
    {
        yield return new MappingStart(mapping.Anchor, mapping.Tag, false, mapping.Style);
        foreach (var pair in mapping.Children)
        {
            foreach (var evt in ConvertToEventStream(pair.Key))
            {
                yield return evt;
            }
            foreach (var evt in ConvertToEventStream(pair.Value))
            {
                yield return evt;
            }
        }
        yield return new MappingEnd();
    }
}

Once you have this, it is trivial to create an adapter for IParser, since the two interfaces are basically equivalent:

public class EventStreamParserAdapter : IParser
{
    private readonly IEnumerator<ParsingEvent> enumerator;

    public EventStreamParserAdapter(IEnumerable<ParsingEvent> events)
    {
        enumerator = events.GetEnumerator();
    }

    public ParsingEvent Current
    {
        get
        {
            return enumerator.Current;
        }
    }

    public bool MoveNext()
    {
        return enumerator.MoveNext();
    }
}

You can then use the adapter to deserialize from any YamlStream, YamlDocument or YamlNode:

var stream = new YamlStream();
stream.Load(new StringReader(input));

var deserializer = new DeserializerBuilder()
    .WithNamingConvention(new CamelCaseNamingConvention())
    .Build();

var prefs = deserializer.Deserialize<YOUR_TYPE>(
    new EventStreamParserAdapter(
        YamlNodeToEventStreamConverter.ConvertToEventStream(stream)
    )
);
Comments