ImmutableオブジェクトのJSONシリアライズ、デシリアライズ(C#)

今回はNewtonsoft.Jsonを利用して、Immutableオブジェクトをシリアライズ、デシリアライズしてみたいと思います。

.NetにはDataContractJsonSerializerが標準で用意されていますが、DataContract、DataMember属性を付加する必要があり、動作のカスタマイズが大変そうであったため、Newtonsoft.Jsonを採用しました。

対象オブジェクト

下記のクラスをシリアライズ、デシリアライズしてみたいと思います。

FullNameは導出項目のため、シリアライズ対象から外される必要があります。

public class Person
{
    public int Age { get; }
    public string FirstName { get; }
    public string LastName { get; }
    public Person Child { get; }
    public string FullName =>
        FirstName + " " + LastName;
    public Person(int age, string firstName, string lastName, Person child)
    {
        Age = age;
        FirstName = firstName;
        LastName = lastName;
        Child = child;
    }
}
var person = new Person(35, "John", "Smith",
                 new Person(10, "Richard", "Smith", null));


Newtonsoft.Json取得

NuGetでNewtonsoft.Jsonを取得します。
f:id:any-programming:20170606093411p:plain

シリアライズ

デフォルトでは、publicなgetterからの値をシリアライズします。

そのため、FullNameもシリアライズされてしまいます。

そこで導出項目は無視するようContractResolverを独自に定義します。

class SerializeResolver : DefaultContractResolver
{
    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        var p = base.CreateProperty(member, memberSerialization);
        var field = member.DeclaringType.GetField($"<{member.Name}>k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic);

        // setterがなく、BackingFieldも持たないものは無視
        p.Ignored = field == null && p.Writable == false;
        return p;
    }
}

シリアライズは、このContractResolverを利用して下記のように記述できます。

var setting = new JsonSerializerSettings() { ContractResolver = new SerializeResolver() };
string json = JsonConvert.SerializeObject(person, setting);

問題なくシリアライズされていることが確認できます。(Visual StudioJSONビューワで確認)
f:id:any-programming:20170606123527p:plain

シリアライズ

デフォルトではsetterが存在しないプロパティはデシリアライズできません。

そこでプロパティが復元できるよう、独自のContractResolverを定義します。

class DeserializeResolver : DefaultContractResolver
{
    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        var p = base.CreateProperty(member, memberSerialization);
        if (p.Writable == false)
        {
            var field = member.DeclaringType.GetField($"<{member.Name}>k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic);
            if (field != null)
            {
                // BackingFieldが存在する場合、ValueProviderをセット
                p.ValueProvider = new FieldValueProvider(field);
                p.Writable = true;
            }
        }
        return p;
    }
}

FieldValueProviderは下記のように定義できます。

class FieldValueProvider : IValueProvider
{
    FieldInfo _field;
    public FieldValueProvider(FieldInfo field) =>
        _field = field;
    public object GetValue(object target) =>
        _field.GetValue(target);
    public void SetValue(object target, object value) =>
        _field.SetValue(target, value);
}

シリアライズは、このContractResolverを利用して下記のように記述できます。

var setting = new JsonSerializerSettings() { ContractResolver = new DeserializeResolver() };
var person = JsonConvert.DeserializeObject<Person>(json, seting);


BackingFieldを使わない場合

BackingFieldはコンパイラが勝手に作成するFieldのため、名称が変わる可能性は0ではありません。

心配な場合は、private setを追加して独自のContractResolverを定義してください。

Fieldのシリアライズ、デシリアライズ

JsonProperty属性を付加すれば対象とみなされるようになります。

[JsonProperty]
int _field;