Search code examples
c#serializationyamlyamldotnet

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.


Solution

  • 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)
        )
    );