Search code examples
c#yamlyamldotnet

YamlDotNet: How do I deserialize either a Sequence or a Mapping?


There's a YAML configuration file that my application loads:

sonarr:
- base_url: abc1
  api_key: xyz1
- base_url: abc2
  api_key: xyz2

I want to change the schema for this to use a mapping (for named instances) rather than an array. Additionally, I want to continue to support array-style (with a deprecation message) for backward compatibility. So the sonarr content can either be a mapping or a sequence. The new schema would look like this:

sonarr:
  instance1:
    base_url: abc1
    api_key: xyz1
  instance2:
    base_url: abc2
    api_key: xyz2

I've spent hours googling and trying different solutions. Nothing seems to work. The approach I was trying was something like this:

public IEnumerable<T> LoadFromStream(TextReader stream, string configSection)
{
    var parser = new Parser(stream);
    parser.Consume<StreamStart>();
    parser.Consume<DocumentStart>();
    parser.Consume<MappingStart>();

    var validConfigs = new List<T>();
    while (parser.TryConsume<Scalar>(out var key))
    {
        if (key.Value != configSection)
        {
            parser.SkipThisAndNestedEvents();
            continue;
        }

        var evt = parser.Consume<NodeEvent>();
        var configs = evt switch
        {
            SequenceStart => _deserializer.Deserialize<Dictionary<string, T>>(parser)
                .Select(kvp =>
                {
                    kvp.Value.Name = kvp.Key;
                    return kvp.Value;
                })
                .ToList(),
            MappingStart => _deserializer.Deserialize<List<T>>(parser),
            _ => null
        };

        if (configs is not null)
        {
            ValidateConfigs(configSection, configs, validConfigs);
        }

        parser.SkipThisAndNestedEvents();
    }

    return validConfigs;
}

However, this won't work because the Consume and TryConsume methods eat the MappingStart / SequenceStart nodes, which makes it impossible to deserialize using List/Dictionary. I think to make this work I need a Consume that is more like a peek.

How should I go about handling this situation, or more generally, flexible schemas like this?


Solution

  • Parser does has a Peek function, which is marked as obsolete, but has been replaced with an Accept function which does basically the same thing with an out parameter instead. However, the easiest thing to do would be to just switch on parser.Current:

    configs = parser.Current switch
    {
        MappingStart  => serializer.Deserialize<Dictionary<string, T>>(parser)
                            .Select(kvp =>
                            {
                                kvp.Value.Name = kvp.Key;
                                return kvp.Value;
                            }),
        SequenceStart => serializer.Deserialize<List<T>>(parser),
        _             => null
    };
    

    Make sure to drop the parser.Consume call before this.