かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

Json.NET で enum を文字列で保存したり数字で保存したりするものを混在させたい

Json.NET は便利ですよね。ということで、こういう感じのクラスを…

public enum Size
{
    Small = 100,
    Large = 1000,
}

public enum TargetType
{
    None,
    TypeA,
    TypeB,
    TypeC,
}

public class Target
{
    public string Name { get; set; }
    public Size Size { get; set; }
    public TargetType Type { get; set; }
}

Size プロパティは数字としてシリアライズしつつ、Type プロパティは enum の文字列としてシリアライズしたい…!というケースがあるとします。Json.NET には、StringEnumConverter というクラスがあるので、これを指定すれば enum を文字列としてシリアライズしてくれるようになります。

JsonConvert.SerializeObject, JsonConvert.DeserializeObject あたりでこれを指定すると Size プロパティが数字じゃなくなってしまうのでプロパティ単位で指定する必要があります。これは JsonConverterAttribute でやるのがいい感じです。

using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using System;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            var target = new Target
            {
                Name = "Tanaka",
                Size = Size.Large,
                Type = TargetType.TypeA,
            };

            var json = JsonConvert.SerializeObject(target);
            Console.WriteLine(json);

            var deserializedTarget = JsonConvert.DeserializeObject<Target>(json);
            Console.WriteLine($"{deserializedTarget.Name}, {deserializedTarget.Size}");
        }
    }

    public enum Size
    {
        Small = 100,
        Large = 1000,
    }

    public enum TargetType
    {
        None,
        TypeA,
        TypeB,
        TypeC,
    }

    public class Target
    {
        public string Name { get; set; }
        public Size Size { get; set; }
        [JsonConverter(typeof(StringEnumConverter))]
        public TargetType Type { get; set; }
    }
}

このように、シリアライズ/デシリアライズのときの挙動をカスタマイズしたいプロパティにだけ JsonConverterAttribute を指定すれば OK です。

属性で汚染されたくない…!

あっ、はい。 Json.NET の属性は付けれないときもありますよね。自分が変更できないクラスとか。

JsonConverter で頑張る

JsonConverter で Target オブジェクトのシリアライズ/デシリアライズの方法をカスタマイズする方法です。 ちょっとメンドクサイ…。

ただただ、純粋にメンドクサイ。

ContractResolver で頑張る

要は特定のプロパティだけ後から JsonConverter つけれればいいので… DefaultContractResolver あたりを継承して GetProperty メソッドをオーバーライドして、狙ったプロパティのときだけ Converter を差し込んでやるとう作戦です。

using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
using System;
using System.Reflection;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            var target = new Target
            {
                Name = "Tanaka",
                Size = Size.Large,
                Type = TargetType.TypeA,
            };

            var json = JsonConvert.SerializeObject(target, new JsonSerializerSettings
            {
                ContractResolver = new MyContractResolver(),
            });
            Console.WriteLine(json);

            var deserializedTarget = JsonConvert.DeserializeObject<Target>(json, new JsonSerializerSettings
            {
                ContractResolver = new MyContractResolver(),
            });
            Console.WriteLine($"{deserializedTarget.Name}, {deserializedTarget.Size}");
        }
    }

    public class MyContractResolver : DefaultContractResolver
    {
        protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
        {
            var p = base.CreateProperty(member, memberSerialization);
            if (member.DeclaringType == typeof(Target) && member.Name == nameof(Target.Type))
            {
                p.Converter = new StringEnumConverter();
            }

            return p;
        }
    }

    public enum Size
    {
        Small = 100,
        Large = 1000,
    }

    public enum TargetType
    {
        None,
        TypeA,
        TypeB,
        TypeC,
    }

    public class Target
    {
        public string Name { get; set; }
        public Size Size { get; set; }
        public TargetType Type { get; set; }
    }
}

enum を辞める

こういう風にしてしまう手もありますね。 Java に enum が無かったころに編み出された enum パターン風に。

using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
using System;
using System.Linq;
using System.Reflection;

namespace CustomJson
{
    class Program
    {
        static void Main(string[] args)
        {
            var target = new Target
            {
                Name = "Tanaka",
                Size = Size.Large,
                Type = TargetType.TypeA,
            };

            var json = JsonConvert.SerializeObject(target, new SizeJsonConverter(),
                new StringEnumConverter());
            Console.WriteLine(json);

            var deserializedTarget = JsonConvert.DeserializeObject<Target>(json, new SizeJsonConverter(), new StringEnumConverter());
            Console.WriteLine($"{deserializedTarget.Name}, {deserializedTarget.Size.Value}");
        }
    }

    public class SizeJsonConverter : JsonConverter
    {
        public override bool CanConvert(Type objectType)
        {
            return typeof(Size) == objectType;
        }

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            var jToken = JToken.Load(reader);
            int value = jToken.Value<int>();
            return typeof(Size).GetTypeInfo()
                .GetProperties(BindingFlags.Static | BindingFlags.Public)
                .Where(x => x.PropertyType == typeof(Size))
                .Select(x => (Size)x.GetValue(null))
                .First(x => x.Value == value);
        }

        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            var size = (Size)value;
            writer.WriteValue(size.Value);
        }
    }

    public class Target
    {
        public string Name { get; set; }
        public Size Size { get; set; }
        public TargetType Type { get; set; }
    }

    public sealed class Size
    {
        public static Size Large { get; } = new Size(1000);
        public static Size Small { get; } = new Size(100);
        private Size(int value) { this.Value = value; }
        public int Value { get; }
    }

    public enum TargetType
    {
        None, TypeA, TypeB, TypeC
    }
}

Size を enum パターン的に作ってしまって、Size クラスに対して JsonConverter を書く感じです。これならどうだろう?

まとめ

おとなしくクラスに JsonConverter を指定しましょう。