You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

YamlDotNet如何反序列化Sequence或Mapping类型的配置?

问题描述

我的应用需要加载YAML配置文件,原配置中sonarr是数组(Sequence)形式:

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

为支持命名实例,我想将其改为映射(Mapping)形式,同时兼容旧的数组形式(并给出弃用提示),新配置如下:

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

我尝试了以下C#代码,但无法正常工作:

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;
}

问题在于Consume和TryConsume会消耗MappingStart/SequenceStart节点,导致后续无法用List<T>或Dictionary<string, T>进行反序列化。我需要类似“peek”的功能判断节点类型,再做对应反序列化,请问该如何处理这种场景,或者有没有更通用的灵活Schema方案?

解决方案

1. 用Parser的Peek功能判断节点类型

YAML.NET的Parser类提供Peek方法,可查看下一个事件而不消耗它。你可以先用Peek判断sonarr对应的节点类型,再选择反序列化方式,注意不要提前消耗关键节点:

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;
        }

        // 先Peek下一个事件,判断类型
        var nextEvent = parser.Peek();
        List<T> configs = null;

        if (nextEvent is SequenceStart)
        {
            // 处理旧数组格式,给出弃用提示
            configs = _deserializer.Deserialize<List<T>>(parser);
            Console.WriteLine($"警告:{configSection}的数组格式已弃用,请使用命名实例的映射格式。");
            // 给旧实例设置默认名称
            for (int i = 0; i < configs.Count; i++)
            {
                configs[i].Name = $"instance{i+1}";
            }
        }
        else if (nextEvent is MappingStart)
        {
            // 处理新映射格式
            var instanceDict = _deserializer.Deserialize<Dictionary<string, T>>(parser);
            configs = instanceDict.Select(kvp =>
            {
                kvp.Value.Name = kvp.Key;
                return kvp.Value;
            }).ToList();
        }

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

    return validConfigs;
}

2. 自定义类型转换器兼容两种格式

创建IYamlTypeConverter实现类,统一处理两种格式的反序列化,更符合YAML.NET的扩展机制:

步骤1:实现自定义转换器

public class ConfigListConverter<T> : IYamlTypeConverter where T : class, new()
{
    private readonly IDeserializer _deserializer;
    private readonly string _configSectionName;

    public ConfigListConverter(IDeserializer deserializer, string configSectionName)
    {
        _deserializer = deserializer;
        _configSectionName = configSectionName;
    }

    public bool Accepts(Type type)
    {
        return type == typeof(List<T>);
    }

    public object ReadYaml(IParser parser, Type type)
    {
        var configs = new List<T>();
        var nextEvent = parser.Peek();

        if (nextEvent is SequenceStart)
        {
            // 解析旧数组格式
            parser.Consume<SequenceStart>();
            while (parser.TryConsume<MappingStart>(out _))
            {
                var config = _deserializer.Deserialize<T>(parser);
                configs.Add(config);
                parser.Consume<MappingEnd>();
            }
            parser.Consume<SequenceEnd>();
            Console.WriteLine($"警告:{_configSectionName}的数组格式已弃用,请使用命名实例格式。");
            // 设置默认名称
            for (int i = 0; i < configs.Count; i++)
            {
                var prop = typeof(T).GetProperty("Name");
                prop?.SetValue(configs[i], $"instance{i+1}");
            }
        }
        else if (nextEvent is MappingStart)
        {
            // 解析新映射格式
            parser.Consume<MappingStart>();
            while (parser.TryConsume<Scalar>(out var instanceName))
            {
                var config = _deserializer.Deserialize<T>(parser);
                var prop = typeof(T).GetProperty("Name");
                prop?.SetValue(config, instanceName.Value);
                configs.Add(config);
            }
            parser.Consume<MappingEnd>();
        }

        return configs;
    }

    public void WriteYaml(IEmitter emitter, object value, Type type)
    {
        // 序列化时输出新映射格式
        var configs = (List<T>)value;
        emitter.Emit(new MappingStart());
        foreach (var config in configs)
        {
            var prop = typeof(T).GetProperty("Name");
            var name = prop?.GetValue(config)?.ToString() ?? $"instance{configs.IndexOf(config)+1}";
            emitter.Emit(new Scalar(name));
            _deserializer.Serialize(emitter, config);
        }
        emitter.Emit(new MappingEnd());
    }
}

步骤2:注册转换器并使用

// 初始化反序列化器时注册转换器
var deserializer = new DeserializerBuilder()
    .WithConverter(new ConfigListConverter<SonarrConfig>(_deserializer, "sonarr"))
    .Build();

// 直接反序列化整个配置类
var appConfig = deserializer.Deserialize<AppConfig>(stream);
// AppConfig类定义示例:
// public class AppConfig { public List<SonarrConfig> Sonarr { get; set; } }

3. 关键注意事项

  • 不要提前消耗SequenceStart/MappingStart节点,确保反序列化器能完整读取节点内容。
  • 处理旧格式必须添加弃用提示,引导用户迁移到新格式。
  • 自定义转换器要正确处理节点的开始和结束事件,避免解析异常。

内容的提问来源于stack exchange,提问作者void.pointer

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 12:03:30