Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

YamlDotNet deserialization of deeply nested dynamic structures

Tags:

c#

yamldotnet

I have a deeply nested object model:

public class CheatSheet {
    public string Leader { get; set; }
    public List<Section> Sections { get; set; }
}

public class Section {
    public string Title { get; set; }
    public List<SubSection> SubSections { get; set; }
}

public class SubSection {
    public string Title { get; set; }
    public List<Cheat> Cheats { get; set; }
}

public class Cheat {
    public string Affected { get; set; }
    public string Text { get; set; }
    public string Hint { get; set; }
    public string Url { get; set; }
}

And I have serialized this to YAML without any problems:

var serializer = new YamlDotNet.Serialization.Serializer();
var sb = new StringBuilder();
var sw = new StringWriter(sb);
serializer.Serialize(sw, model);
string yaml = sb.ToString();

The yaml looks good, very similar to a JSON or HJSON representation.

I now want to deserialize it - n.b. I want to deserialize it into a dynamic object NOT into the original model (which is only being used in this example to generate the YAML in the first place, it won't exist in the final assembly).

var sr = new StringReader(yaml);
var deserializer = new YamlDotNet.Serialization.Deserializer();
dynamic expando = deserializer.Deserialize<ExpandoObject>(sr);

The problem is that the resulting expando is very difficult to use, containing many unnecessary levels of nesting. For example:

expando.Sections[0]["Title"]
expando.Sections[0]["SubSections"][0]["Title"]
expando.Sections[0]["SubSections"][0]["Cheats"][0]["Text"]

But I would like this to be

expando.Sections[0].Title
expando.Sections[0].SubSections[0].Title
expando.Sections[0].SubSections[0].Cheats[0].Text

Is this possible in any way?

There is a repro program available at https://github.com/PhilipDaniels/Lithogen in the project Gitcheatsheet.TestHarness, at commit 2db9a0491e8ab50bb07aee552ddec6697c4b8bfc

like image 249
Philip Daniels Avatar asked Dec 11 '25 13:12

Philip Daniels


2 Answers

Well I answered my own question, to an extent. This class will do what I asked for my document (not tested on others). Easy to expand to multiple documents per YAML string. Could probably improve the handling of scalars by trying to convert to double, DateTime etc.

Surely there is a better way of doing this, but the API for this project is very confusing.

public static class YamlUtils
{
    /// <summary>
    /// Converts a YAML string to an <code>ExpandoObject</code>.
    /// </summary>
    /// <param name="yaml">The YAML string to convert.</param>
    /// <returns>Converted object.</returns>
    public static ExpandoObject ToExpando(string yaml)
    {
        using (var sr = new StringReader(yaml))
        {
            var stream = new YamlStream();
            stream.Load(sr);
            var firstDocument = stream.Documents[0].RootNode;
            dynamic exp = ToExpando(firstDocument);
            return exp;
        }
    }

    /// <summary>
    /// Converts a YAML node to an <code>ExpandoObject</code>.
    /// </summary>
    /// <param name="node">The node to convert.</param>
    /// <returns>Converted object.</returns>
    public static ExpandoObject ToExpando(YamlNode node)
    {
        ExpandoObject exp = new ExpandoObject();
        exp = (ExpandoObject)ToExpandoImpl(exp, node);
        return exp;
    }

    static object ToExpandoImpl(ExpandoObject exp, YamlNode node)
    {
        YamlScalarNode scalar = node as YamlScalarNode;
        YamlMappingNode mapping = node as YamlMappingNode;
        YamlSequenceNode sequence = node as YamlSequenceNode;

        if (scalar != null)
        {
            // TODO: Try converting to double, DateTime and return that.
            string val = scalar.Value;
            return val;
        }
        else if (mapping != null)
        {
            foreach (KeyValuePair<YamlNode, YamlNode> child in mapping.Children)
            {
                YamlScalarNode keyNode = (YamlScalarNode)child.Key;
                string keyName = keyNode.Value;
                object val = ToExpandoImpl(exp, child.Value);
                exp.SetProperty(keyName, val);
            }
        }
        else if (sequence != null)
        {
            var childNodes = new List<object>();
            foreach (YamlNode child in sequence.Children)
            {
                var childExp = new ExpandoObject();
                object childVal = ToExpandoImpl(childExp, child);
                childNodes.Add(childVal);
            }
            return childNodes;
        }

        return exp;
    }
}

where SetProperty is an extension method, for some reason I can't recall:

public static void SetProperty(this IDictionary<string, object> target, string name, object thing)
{
    target[name] = thing;
}

Beware! This code has not been fully tested! There are probably some edge conditions.

like image 143
Philip Daniels Avatar answered Dec 14 '25 03:12

Philip Daniels


A solution more in line with the library would be to replace the default type resolver DefaultContainersNodeTypeResolver with a custom resolver, that resolves mappings to ExpandoObject instead of Dictionary<object, object>. See below:

using System;
using System.Collections.Generic;
using System.Dynamic;
using YamlDotNet.Core.Events;
using YamlDotNet.Serialization;

public class ExpandoNodeTypeResolver : INodeTypeResolver
{
    public bool Resolve(NodeEvent nodeEvent, ref Type currentType)
    {
        if (currentType == typeof(object))
        {
            if (nodeEvent is SequenceStart)
            {
                currentType = typeof(List<object>);
                return true;
            }
            if (nodeEvent is MappingStart)
            {
                currentType = typeof(ExpandoObject);
                return true;
            }
        }

        return false;
    }
}

To use it, do this:

var deserializer = new DeserializerBuilder()
            .WithNodeTypeResolver(
                  new ExpandoNodeTypeResolver(), 
                  ls => ls.InsteadOf<DefaultContainersNodeTypeResolver>())
            .Build();
dynamic result = deserializer.Deserialize(input);
like image 25
Rok Avatar answered Dec 14 '25 04:12

Rok



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!