在JSON.NET中序列化null

48

在使用JSON.NET序列化任意数据时,任何null属性都会被写入JSON中:

"propertyName" : null

这当然是正确的。

但是我有一个要求,即将所有null自动转换为默认的空值。例如,null的字符串应该变成String.Empty,null的 int?应该变成0,null的bool? 应该变为false等等。

NullValueHandling不太有用,因为我既不想忽略null,也不想包含它们(嗯,这是个新特性吗?)。

所以我转而实现了一个自定义的JsonConverter。虽然实现本身很容易,但不幸的是,这仍然不起作用——对于具有null值的属性,CanConvert()从未被调用,因此WriteJson()也不会被调用。显然,null会被自动直接序列化为null,而不经过自定义管道。

例如,以下是一个处理null字符串的自定义转换器示例:

public class StringConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return typeof(string).IsAssignableFrom(objectType);
    }

    ...
    public override void WriteJson(JsonWriter writer, 
                object value, 
                JsonSerializer serializer)
    {
        string strValue = value as string;

        if (strValue == null)
        {
            writer.WriteValue(String.Empty);
        }
        else
        {
            writer.WriteValue(strValue);
        }
    }
}

在调试器中逐步执行代码时,我注意到对于具有null值的属性,这两种方法都不会被调用。

深入研究了JSON.NET的源代码后(显然,我没有深入研究)发现了一个特殊情况的检查,它明确调用.WriteNull()

值得一提的是,我尝试实现了一个自定义的JsonTextWriter并覆盖了默认的.WriteNull()方法的实现...

public class NullJsonWriter : JsonTextWriter
{
    ... 
    public override void WriteNull()
    {
        this.WriteValue(String.Empty);
    }
}
然而,这种方法效果不佳,因为WriteNull() 方法对底层数据类型一无所知。因此,当出现 null 的时候,我可以输出 "",但是对于例如int、bool等类型来说就不太适用了。
那么我的问题是:除非手动转换整个数据结构,否则是否存在任何解决方案或解决方法?

2
在您需要该类型的默认值的情况下,是否可以覆盖WriteValue(Nullable<T>)中的任何一个?对于int类型,相应的方法是WriteValue(Nullable<int>)还是直接流到WriteNull()? JsonTextWriter - ShelbyZ
1
经过对Json.Net源代码的粗略查看,我认为如果不修改其源代码,很难轻松地实现这一点。 - svick
1
@ShelbyZ if(value==null) 这基本上是第一种情况 :( - J. Holmes
1
哎呀,我希望有一种方法可以在属性上指定了JsonConverterAttribute的情况下序列化NULL。 我需要在我的空属性上修改输出,而不仅仅是使用“null”。也许我们可以为此提出一个功能请求。 - Brian Chavez
哇,十年过去了....!现在有这个:https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-converters-how-to?pivots=dotnet-6-0#handle-null-values JsonConverter<T>.HandleNull 现在可以让自定义转换器处理 null... ‍♀️ - AviD
显示剩余5条评论
1个回答

27

好的,我认为我想出了一个解决方案(我的第一个解决方案完全不正确,但是当时我在火车上)。你需要创建一个特殊的合同解析器和一个针对可空类型的自定义值提供程序。考虑这个:

public class NullableValueProvider : IValueProvider
{
    private readonly object _defaultValue;
    private readonly IValueProvider _underlyingValueProvider;


    public NullableValueProvider(MemberInfo memberInfo, Type underlyingType)
    {
        _underlyingValueProvider = new DynamicValueProvider(memberInfo);
        _defaultValue = Activator.CreateInstance(underlyingType);
    }

    public void SetValue(object target, object value)
    {
        _underlyingValueProvider.SetValue(target, value);
    }

    public object GetValue(object target)
    {
        return _underlyingValueProvider.GetValue(target) ?? _defaultValue;
    }
}

public class SpecialContractResolver : DefaultContractResolver
{
    protected override IValueProvider CreateMemberValueProvider(MemberInfo member)
    {
        if(member.MemberType == MemberTypes.Property)
        {
            var pi = (PropertyInfo) member;
            if (pi.PropertyType.IsGenericType && pi.PropertyType.GetGenericTypeDefinition() == typeof (Nullable<>))
            {
                return new NullableValueProvider(member, pi.PropertyType.GetGenericArguments().First());
            }
        }
        else if(member.MemberType == MemberTypes.Field)
        {
            var fi = (FieldInfo) member;
            if(fi.FieldType.IsGenericType && fi.FieldType.GetGenericTypeDefinition() == typeof(Nullable<>))
                return new NullableValueProvider(member, fi.FieldType.GetGenericArguments().First());
        }

        return base.CreateMemberValueProvider(member);
    }
}

然后我使用以下进行了测试:

class Foo
{
    public int? Int { get; set; }
    public bool? Boolean { get; set; }
    public int? IntField;
}

还有以下情况:

[TestFixture]
public class Tests
{
    [Test]
    public void Test()
    {
        var foo = new Foo();

        var settings = new JsonSerializerSettings { ContractResolver = new SpecialContractResolver() };

        Assert.AreEqual(
            JsonConvert.SerializeObject(foo, Formatting.None, settings), 
            "{\"IntField\":0,\"Int\":0,\"Boolean\":false}");
    }
}

希望这能有所帮助...

编辑 - 更好地识别一个Nullable<>类型

编辑 - 添加了对字段和属性的支持,同时借助常规的DynamicValueProvider来完成大部分工作,并更新了测试


还需要为String加入特殊情况,因为它实际上不是Nullable<>类型... - AviD
1
@AviD 啊,我忘了你还需要翻译字符串。但是它的实现方式差不多。你可以为字符串创建一个新的ValueProvider,或者改变现有的签名,而不是传递通用类型,只需传递默认值(并将“Activator.CreateInstance()”向上移动一级)。 - J. Holmes
1
@AviD和我都认为,这并不像我想象中的那么直观,但这是我能想到的唯一解决方案,它适用于JSON.NET现有的架构,并且不需要对库本身进行任何更改。 - J. Holmes
1
我知道这里不做这个,但是在漫长的一天之后:谢谢! - Guillaume Beauvois
哇,十年过去了...!现在有这个:https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-converters-how-to?pivots=dotnet-6-0#handle-null-values JsonConverter<T>.HandleNull 现在可以让自定义转换器处理 null... ‍♀️ - AviD
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接