web-dev-qa-db-ja.com

JSON.NETでnullをシリアル化する

JSON.NETを介して任意のデータをシリアル化する場合、nullのプロパティはJSONに次のように書き込まれます。

「propertyName」:null

もちろんこれは正しいです。

ただし、すべてのヌルをデフォルトの空の値に自動的に変換する必要があります。 null stringsは_String.Empty_に、null _int?_ sは_0_に、null _bool?_ sはfalseなどになります。

NullValueHandlingは役に立たない、なぜならIgnore nullを使いたくはないが、Includeそれらもしたくない(うーん、新機能?)。

そこで、カスタムJsonConverterを実装することにしました。
実装自体は簡単でしたが、残念ながらこれはまだ機能しませんでした-CanConvert()はnull値を持つプロパティに対して呼び出されることはないため、WriteJson()は呼び出されませんどちらか。どうやらヌルは、カスタムパイプラインなしで、自動的に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のソースコードを掘り下げてみたところ(明らかに、深く掘り下げたことはありませんでした)、nullをチェックし、.WriteNull()を明示的に呼び出す特別なケースがあることがわかりました。

それが価値があるもののために、カスタムJsonTextWriterを実装し、デフォルトの.WriteNull()実装をオーバーライドしてみました...

_public class NullJsonWriter : JsonTextWriter
{
    ... 
    public override void WriteNull()
    {
        this.WriteValue(String.Empty);
    }
}
_

ただし、WriteNull()メソッドは基になるデータ型について何も知らないため、これはうまく機能しません。確かに、任意のnullに対して_""_を出力できますが、それは例えばint、boolなど。

だから、私の質問-データ構造全体を手動で変換するのではなく、これに対する解決策や回避策はありますか?

46
AviD

さて、私は解決策を思いついたと思います(最初の解決策はまったく正しくありませんでしたが、再び電車に乗っていました)。 Nullable型の特別なコントラクトリゾルバーとカスタムValueProviderを作成する必要があります。このことを考慮:

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

うまくいけば、これが少し役立つでしょう...

編集– a Nullable<>タイプ

編集–フィールドとプロパティのサポートを追加し、通常のDynamicValueProviderに加えて、ほとんどの作業を行い、更新されたテストでピギーバッキング

26
J. Holmes