精:为Newtonsoft.Json实现一个属性支持多别名的契约解释器

2023-08-29 09:12:07 浏览数 (2)

大家也许知道使用Newtonsoft.Json反序列化json为对象的时候,如果json的key和对象的属性名不匹配,可以使用[JsonProperty]给属性配置别名,但是JsonProperty有个缺点,就是只能设置一个别名,不能设置多个别名,并且如果用JsonProperty设置了别名之后,它本身的名字也不能用了,所以有时候不能满足业务的需要,比如如下的两个json并不能反序列化成同一个OrderItem对象:

代码语言:javascript复制
{    "skuid":"1"}
{    "productId":"1"}
public class OrderItem
{
    public string SkuId { get; set; }
}

这种情况下我们就需要给OrderItem的SkuId设置两个别名,即skuid和productId,而Newtonsoft.Json本身是不支持的,所以我们需要自己实现一个ContractResolver,使用的时候为JsonConvert配置JsonSerializerSettings指定ContractResolver用我们自己实现的即可,同时我们再实现一个对标JsonProperty的Attribute。

既然我们要实现一个属性对应多个别名,那么我们肯定是希望配置的时候这样写:[XxxJsonProperty("a","b","c","d")],并且至少要指定一个别名,所以它至少需要两个参数,一个必填的string,一个可变长度的数组即可,我们把Attribute起名为FallbackJsonPropertyAttribute吧。

FallbackJsonPropertyAttribute

代码语言:javascript复制
[AttributeUsage(AttributeTargets.Property)]
public class FallbackJsonProperty : Attribute
{
    public string PreferredName { get; }
    public string[] FallbackReadNames { get; }

    public FallbackJsonProperty(string preferredName, params string[] fallbackReadNames)
    {
        PreferredName = preferredName;
        FallbackReadNames = fallbackReadNames;
    }
}

接下来就基于这个Attribute实现我们自己的契约解释器ContractResolver,通常情况下,json的key命名风格都是驼峰命名的,所以我们直接写一个继承自CamelCasePropertyNamesContractResolver的ContractResolver即可,就叫FallbackJsonPropertyResolver吧:

Newtonsoft.Json自定义ContractResolver示例:

https://www.newtonsoft.com/json/help/html/CustomContractResolver.htm

FallbackJsonPropertyResolver

接下来就按官方示例写一个吧,而官方文档里的也正好是操作属性的,所以先抄一个模板吧

代码语言:javascript复制
using System.Text.Json;
 
/// <summary>
/// 多别名属性的解释器
/// </summary>
public class FallbackJsonPropertyResolver : CamelCasePropertyNamesContractResolver
{
    protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
    {
    }
}

CreateProperties函数接受了两个参数,其中有一个Type类型的参数,对应的肯定是被反序列的对象的类型,所以我们可以根据传入的type很容易得到类的所有成员信息,然后找到成员中被FallbackJsonPropertyAttribute标记的成员,检查有几个别名,把多余的别名挨个做解析就可以了,将别名属性添加到List<JsonProperty>中,就这么简单。

代码语言:javascript复制
using System.Text.Json;
 
/// <summary>
/// 多别名属性的解释器
/// </summary>
public class FallbackJsonPropertyResolver : CamelCasePropertyNamesContractResolver
{
    protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
    {
        var typeMembers = GetSerializableMembers(type);
        var properties = new List<JsonProperty>();
        foreach (var member in typeMembers)
        {
            var property = CreateProperty(member, memberSerialization);
            properties.Add(property);
            var fallbackAttribute = member.GetCustomAttribute<FallbackJsonProperty>();
            if (fallbackAttribute == null)
            {
                continue;
            }
 
            property.PropertyName = fallbackAttribute.PreferredName;
            foreach (var alternateName in fallbackAttribute.FallbackReadNames)
            {
                var fallbackProperty = CreateProperty(member, memberSerialization);
                fallbackProperty.PropertyName = alternateName;
                properties.Add(fallbackProperty);
            }
        }
 
        return properties;
    }
}

使用

然后就可以在调用json反序列的时候这样用:

代码语言:javascript复制
public class MyClass
{
    [FallbackJsonProperty(nameof(MyProperty), "a", "b")]
    public string MyProperty { get; set; }
}
代码语言:javascript复制
string json1 = "{"a":"aa"}";
string json2 = "{"b":"bb"}";
string json3 = "{"MyProperty":"mm"}";
JsonConvert.DefaultSettings = () => new JsonSerializerSettings()
{
    ContractResolver = new FallbackJsonPropertyResolver()
};
var m1 = JsonConvert.DeserializeObject<MyClass>(json1);
var m2 = JsonConvert.DeserializeObject<MyClass>(json2);
var m3 = JsonConvert.DeserializeObject<MyClass>(json3);

还不过瘾?再实现一个让类的某个属性能够被反序列化但不能够序列化的契约解释器ContractResolver,并且融合上面的功能:

CompositeContractResolver组合契约解释器

能够被反序列化但不能够序列化,那Attribute就叫SerializeIgnoreAttribute吧

代码语言:javascript复制
/// <summary>
/// 序列化时忽略
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class SerializeIgnoreAttribute : Attribute
{
}

为了让CompositeContractResolver包含FallbackJsonPropertyResolver的功能,直接让CompositeContractResolver继承FallbackJsonPropertyResolver,重写CreateProperty函数即可:

代码语言:javascript复制
/// <summary>
/// 支持只允许反序列化属性和多别名属性的解释器
/// </summary>
public class CompositeContractResolver : FallbackJsonPropertyResolver
{
    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        var property = base.CreateProperty(member, memberSerialization);
        if (property is {Writable: true})
        {
            var attributes = property.AttributeProvider.GetAttributes(typeof(SerializeIgnoreAttribute), true);
            if (attributes.Any())
            {
                property.ShouldSerialize = _ => false;
            }
        }
 
        return property;
    }
}

因为FallbackJsonPropertyResolver里面的CreateProperties调用了CreateProperty,所以直接重写CreateProperty,检测对应的属性有没有被SerializeIgnore标记,如果被标记,那就将其设置为不可序列化即可,即对应的代码:property.ShouldSerialize = _ => false;

总结

Newtonsoft.Json虽然绝大多数情况下都是满足业务需求的,即使不满足写个自定义的ContractResolver也能轻松解决,不得不说是真的好用!本文的功能也早已集成到了开源项目:Masuit.Tools中,大家有需要也不必抄代码了,直接装nuget包即可。

想看完整源代码的,在这里:https://github.com/ldqk/Masuit.Tools/tree/master/Masuit.Tools.Abstractions/Systems

0 人点赞