如何将不匹配的标签序列化为字典?

时间:2014-12-24 23:49:49

标签: c# .net yaml yamldotnet

我正在尝试使用YamlDotNet解析此YAML文档:

title: Document with dynamic properties
/a:
  description: something
/b:
  description: other something

进入这个对象:

public class SimpleDoc
{

    [YamlMember(Alias = "title")]
    public string Title { get; set;}

    public Dictionary<string, Path> Paths { get; set; }
}

public class Path
{
    [YamlMember(Alias = "description")]
    public string Description {get;set;}
}

我希望/ a / b或任何其他不匹配的属性最终在Paths字典中。如何配置YamlDotNet以支持此方案?

反序列化设置为:

// file paths is the path to the specified doc :)
var deserializer = new Deserializer();
var doc = deserializer.Deserialize<SimpleDoc>(File.OpenText(filePath));

然而,由于SimpleDoc上没有属性 / a ,它失败了。如果我将构造函数中的 ignoreUnmatched 设置为true,则会按预期忽略它。

1 个答案:

答案 0 :(得分:0)

对于一个将YAML模式定义为JSON模式的项目,我需要它,我曾使用它来生成类(通过NJsonSchema)。但是,实际数据为YAML。因此,我需要将YAML反序列化到这些类中。

我利用这种机制,如果一个类实现了IDictionary<string, objectIDictionary<object, object>,则YamlDotNet会将来自该节点的所有键/值放入字典中。如果我是从Dictionary<string, object>派生该类的,那么我将无权修改将值添加到该字典的方式,因此我被迫实现该接口。

internal partial class Language : IDictionary<string, object>
{
    [YamlMember(Alias = "namespace")]
    public string Namespace { get; set; }

    [YamlMember(Alias = "discriminatorValue")]
    public string DiscriminatorValue { get; set; }

    [YamlMember(Alias = "uid")]
    public string Uid { get; set; }

    [YamlMember(Alias = "internal")]
    public bool Internal { get; set; }

    [YamlIgnore]
    public IDictionary<string, object> AdditionalProperties = new Dictionary<string, object>();

    private readonly Dictionary<string, object> _dictionary = new Dictionary<string, object>();
    private static readonly Dictionary<string, PropertyInfo> DeserializableProperties = typeof(Language).GetDeserializableProperties();

    // Workaround for mapping properties from the dictionary entries
    private void AddAndMap(string key, object value)
    {
        _dictionary.Add(key, value);

        if (DeserializableProperties.ContainsKey(key))
        {
            var propInfo = DeserializableProperties[key];
            propInfo.SetValue(this, propInfo.DeserializeDictionary(value));
            return;
        }

        AdditionalProperties.Add(key, value);
    }

    public IEnumerator<KeyValuePair<string, object>> GetEnumerator() => _dictionary.GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

    public void Add(KeyValuePair<string, object> item) => AddAndMap(item.Key, item.Value);
    public void Clear() => _dictionary.Clear();
    public bool Contains(KeyValuePair<string, object> item) => _dictionary.ContainsKey(item.Key);
    public void CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)
    {
        foreach (var item in _dictionary)
        {
            array[arrayIndex++] = item;
        }
    }
    public bool Remove(KeyValuePair<string, object> item) => _dictionary.Remove(item.Key);

    public int Count => _dictionary.Count;
    public bool IsReadOnly => false;
    public void Add(string key, object value) => AddAndMap(key, value);
    public bool ContainsKey(string key) => _dictionary.ContainsKey(key);
    public bool Remove(string key) => _dictionary.Remove(key);
    public bool TryGetValue(string key, out object value) => _dictionary.TryGetValue(key, out value);

    public object this[string key]
    {
        get => _dictionary[key];
        set => AddAndMap(key, value);
    }

    public ICollection<string> Keys => _dictionary.Keys;
    public ICollection<object> Values => _dictionary.Values;
}

在此示例中,Language是我要反序列化的类。作为生成的类的一部分,我对该类具有其他属性(通过JSON模式生成器)。由于它们是局部的,因此我可以实现此解决方法。

基本上,我创建了AddAndMap并将其应用于添加字典条目的界面中的任何位置。我还创建了AdditionalProperties字典来保存我们的值。请记住,我们需要IDictionary中的所有值才能正确地序列化此类。

接下来,我总是将条目添加到支持_dictionary,然后确定我是否具有向其提供值的属性。如果我没有该值应映射到的属性,则将其放入AdditionalProperties字典中。

要确定我是否有可用的反序列化属性,我必须编写一些扩展方法。

public static Dictionary<string, PropertyInfo> GetDeserializableProperties(this Type type) => type.GetProperties()
    .Select(p => new KeyValuePair<string, PropertyInfo>(p.GetCustomAttributes<YamlMemberAttribute>(true).Select(yma => yma.Alias).FirstOrDefault(), p))
    .Where(pa => !pa.Key.IsNullOrEmpty()).ToDictionary(pa => pa.Key, pa => pa.Value);

// Only allows deserialization of properties that are primitives or type Dictionary<object, object>. Does not support properties that are custom classes.
public static object DeserializeDictionary(this PropertyInfo info, object value)
{
    if (!(value is Dictionary<object, object>)) return TypeConverter.ChangeType(value, info.PropertyType);

    var type = info.PropertyType;
    var properties = type.GetDeserializableProperties();
    var property = Activator.CreateInstance(type);
    var matchedProperties = ((Dictionary<object, object>)value).Where(e => properties.ContainsKey(e.Key.ToString()));
    foreach (var (propKey, propValue) in matchedProperties)
    {
        var innerInfo = properties[propKey.ToString()];
        innerInfo.SetValue(property, innerInfo.DeserializeDictionary(propValue));
    }
    return property;
}

要注意的主要事情是,这仅适用于类的原语或Dictionary<object, object>属性。原因是因为设置属性时,它使用TypeConverter.ChangeType返回适当的对象。如果您有一种方法来解释如何创建自定义类,则只需用该解决方案替换TypeConverter.ChangeType调用即可。

要获取类的可反序列化的属性,我使用反射来获取该类的属性的YamlMemberAttribute。然后,我将该属性的PropertyInfo放入字典中,其中的键是AliasYamlMemberAttribute的值。在我的解决方案中,所有属性都需要为该属性定义别名

然后,我找到一个属性,该属性的别名与从AddAndMap调用中获取的密钥相匹配。递归发生这种情况,以便(将来)可以使用自定义类属性进行此项工作。它检查Dictionary<object, object>作为值类型,因为YamlDotNet在将某些内容反序列化为字典时,总是会将子类反序列化为Dictionary<object, object>

总体

如果您仅尝试反序列化类的平面(无自定义类属性),则此解决方案有效并且可以简化。或者,可以扩展它以支持自定义类属性。也可以将解决方案设置为您的其他类派生的基本类型,并且您将在GetType()中使用DeserializableProperties而不是typeof(Language)。这是一个过于冗长的解决方案,但是直到YamlDotNet拥有一个更简洁/更简单/正确的解决方案之后,这才是我探索其源代码后能想到的最好的解决方案。

最后,请记住,由于您的类是IDictionary的派生类,因此YamlDotNet 不会序列化您类中的任何属性,即使它们具有{{1} }属性。如果YamlMember将提供该功能,则本来我会想到一个更干净的解决方案。 las,事实并非如此。