5

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.

Daniel A.A. Pelsmaeker
  • 47,471
  • 20
  • 111
  • 157

1 Answers1

5

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 IEnumerable<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)
    )
);
Dmitry Fedorkov
  • 4,301
  • 1
  • 21
  • 30
Antoine Aubry
  • 12,203
  • 10
  • 45
  • 74
  • This is great and very complete. Works like a charm! Thanks for taking the time to write it up. – Daniel A.A. Pelsmaeker Nov 21 '16 at 21:43
  • Fantastic - this answer allowed me to work around an issue where it seems anchors deserialized by the Deserializer are then not found when aliases try to reference them later in the document. Note this issue in itself only came up because https://github.com/aaubry/YamlDotNet/issues/295 caused me to need to map my aliases property – RichTea Apr 18 '18 at 21:53