LoginSignup
16
16

More than 5 years have passed since last update.

以前C#でJSONの構文解析器を作成しました。

しかし、この構文解析器で得られるのはobject型として要素が格納された配列やobject型として値が格納された辞書なので、具体的な型の実体に変換するには都度変換処理を書かなければなりません。しかし、具体的な型それぞれについて変換処理を書かなければならないのは非常に面倒です。

出来れば↓のように使えるようにしたいところです。すなわち、具体的な型(ここではPersonクラスやPersonsクラス)のプロパティにJSON形式のオブジェクトから値を取得することができるということを示す属性を付与し、以前作成したJSONの構文解析器で対応するJSON形式の文字列を構文解析してJSON形式のオブジェクトに変換し、具体的な型の名称とJSON形式のオブジェクトを指定することでJSON形式のオブジェクトを指定した具体的な型の実体に変換することができれば便利です。

        public class Person
        {
            [JsonProperty("name")]
            public string Name { get; set; }
            [JsonProperty("age")]
            public int? Age { get; set; }
            [JsonProperty("isMale")]
            public bool IsMale { get; set; }
            [JsonProperty("hobby")]
            public string[] Hobby { get; set; }
            [JsonProperty("father")]
            public Person Father { get; set; }
        }

        public class Persons
        {
            [JsonProperty("persons")]
            public Person[] PersonsArray { get; set; }
        }

        public static void Main(string[] args)
        {
            string json = "{\"persons\": [{\"name\": \"Yumina\", \"age\": 25, \"isMale\": true, \"hobby\": [\"mathematics\", \"programming\"], \"father\": {\"name\": \"Yuusuke\", \"age\": 55, \"isMale\": true, \"hobby\": [], \"father\": null}}, {\"name\": \"Kururu\", \"age\": 5, \"isMale\": false, \"hobby\": [], \"father\": null}]}";

            Dictionary<string, object> osDict = JsonParser.Parse(new Scanner(new Text(json))) as Dictionary<string, object>;

            Persons ps = JsonMapper.Map<Persons>(osDict);
        }

今日は正にこの部分の実装を行いたいと思います。

必要なもの

この機能を実装するために以前作ったものを幾つか利用します。

JsonProperty属性

JsonProperty属性を追加します。JSON形式のオブジェクトから値を取得することができるということを示すためにプロパティに付与する属性です。

    //JSON形式のオブジェクトから値を取得可能なプロパティであることを示す属性
    //プロパティに付与することができる
    [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
    public sealed class JsonPropertyAttribute : Attribute
    {
        public JsonPropertyAttribute()
        {
            Name = Option.Return<string>();
        }

        public JsonPropertyAttribute(string _name)
        {
            Name = Option.Return(_name);
        }

        //JSON形式のオブジェクトにおける名称
        public Option<string> Name { get; private set; }
    }

例外

例外を幾つか追加します。

    //JSON形式のオブジェクトに対応する名称の要素が含まれていない場合に投げられる例外
    public class NotFoundException : Exception
    {
        public NotFoundException(string _message, string _name) : base(_message)
        {
            Name = _name;
        }

        //要素の名称
        public string Name { get; private set; }
    }

    //プロパティの型と要素の型が合わない場合に投げられる例外
    public class TypeMismatchException : Exception { }

    //値の型の変換が失敗した場合に投げられる例外
    public class ConversionException : Exception
    {
        public ConversionException(string _message) : base(_message) { }
    }

JSonMapper静的クラス

JsonMapper静的クラスを実装します。

    public static class JsonMapper
    {
        static JsonMapper()
        {
            //運用記録を作成するためのオブジェクトを作成する
            Logger = new Logger();
        }

        //この機能の正式名称
        private static readonly string FormalName = "JsonMapper";

        //運用記録を作成するためのオブジェクト
        private static Logger Logger { get; set; }
    }

この静的クラスに必要な関数を追加していきます。

最初に、オブジェクトをdouble型の値に変換する関数を追加します。以前作成したJSONの構文解析器では数値をdouble型の値として扱いました。しかし、数値が文字列として格納されている場合も考えられるので、オブジェクトがdouble型である場合とstring型である場合に対応しなければなりません。string型の実体の場合、double.Parseで変換を行います。double型の値の場合はdouble型にキャストして返すだけです。

        //オブジェクトをdouble型の値に変換し、返す
        private static double ObjectToDouble(object o)
        {
            //オブジェクトがstring型の実体である場合
            if (o is string)
            {
                try
                {
                    //オブジェクトをstring型にキャストしてdouble型の値に変換し、返す
                    return double.Parse((string)o);
                }
                //double型の値として解釈できなかった場合には例外を投げる
                catch (FormatException)
                {
                    throw new ConversionException("bad format");
                }
                //double型の値の範囲外であった場合には例外を投げる
                catch (OverflowException)
                {
                    throw new ConversionException("overflow");
                }
                //それ以外の問題が発生した場合にも例外を投げる
                catch (Exception)
                {
                    throw new ConversionException("unexpected error");
                }
            }
            //オブジェクトがdouble型の値である場合にはオブジェクトをdouble型にキャストし、返す
            else if (o is double)
                return (double)o;
            //オブジェクトが何れの型の実体でもない場合には例外を投げる
            else
                throw new TypeMismatchException();
        }

次に、オブジェクトを数値型の値に変換する関数を追加します。以前作成したJSONの構文解析器では数値をdouble型の値として扱ったので、一旦オブジェクトをdouble型の値に変換し、その後、所望の数値型の値に変換します。変換関数は引数として受け取ります。また、逆変換関数や最大値や最小値も引数として受け取ります。最大値と最小値によって値の範囲の確認を行い、逆変換関数によって値の精度の確認を行います。値が有効範囲外の場合はエラーとし、精度が落ちる場合は警告の運用記録を出力します。

        //オブジェクトを数値型の値に変換し、返す
        private static T GetValueSmallerDouble<T>(object o, Func<double, T> converter, Func<T, double> deconverter, double max, double min)
        {
            //オブジェクトをdouble型の値に変換する
            double d = ObjectToDouble(o);

            //値が最大許容値より大きいか、最小許容値より小さい場合には例外を投げる
            if (Math.Floor(d) > max || Math.Ceiling(d) < min)
                throw new ConversionException("overflow");

            //double型の値をT型の実体に変換する
            T b = converter(d);

            //元のdouble型の値とT型の実体をdouble型に変換した値が一致しない(値の精度が落ちた)場合
            if (d != deconverter(b))
            {
                //警告の運用記録を出力する
                const string id = "9e6746a5-42c5-4851-ba2c-ee77f66f4345";
                const string text = "rounded off fraction";
                const LogType type = LogType.Warning;

                Logger.IssueLog(new LogItem(Guid.Parse(id), text, type, FormalName));
            }

            //T型の実体を返す
            return b;
        }

次に、オブジェクトを与えられた型の実体に変換する関数を追加します。全ての基本型と基本型のNullable型に対応しています。数値型の場合、上で作成した関数を使って変換を行います。char型の場合、オブジェクトは1文字の文字列でなければならないものとします。

与えられた型が基本型でも基本型のNullable型でもない場合には再帰的に変換を行います(ただし、オブジェクトの型はJSON形式のオブジェクトの型(Dictionary<string, object>)でなければなりません)。MapObject関数はJSON形式のオブジェクトを与えられた型の実体に変換する関数です。与えられた型が基本型でも基本型のNullable型でもない場合にはこの関数を再帰的に呼び出すことになります。

        //オブジェクトを与えられた型の実体に変換し、返す
        private static object GetValue(object o, Type pType)
        {
            //型がNullable<T>型であり、オブジェクトがnullである場合にはnullを返す
            if (pType.IsGenericType && pType.GetGenericTypeDefinition() == typeof(Nullable<>) && o == null)
                return null;

            //これより下の条件分岐では、型がNullable<T>型である場合には必ずオブジェクトはnull以外となる
            //Nullable<T>型を返さなければならない場合にT型を返しても勝手に変換されるので問題ない

            //型がbyte型又はbyte?型である場合にはオブジェクトをbyte型の値に変換し、返す
            else if (pType == typeof(byte) || pType == typeof(byte?))
                return GetValueSmallerDouble(o, (d) => (byte)d, (b) => b, byte.MaxValue, byte.MinValue);
            //型がsbyte型又はsbyte?型である場合にはオブジェクトをsbyte型の値に変換し、返す
            else if (pType == typeof(sbyte) || pType == typeof(sbyte?))
                return GetValueSmallerDouble(o, (d) => (sbyte)d, (b) => b, sbyte.MaxValue, sbyte.MinValue);
            //型がint型又はint?型である場合にはオブジェクトをint型の値に変換し、返す
            else if (pType == typeof(int) || pType == typeof(int?))
                return GetValueSmallerDouble(o, (d) => (int)d, (b) => b, int.MaxValue, int.MinValue);
            //型がuint型又はuint?型である場合にはオブジェクトをuint型の値に変換し、返す
            else if (pType == typeof(uint) || pType == typeof(uint?))
                return GetValueSmallerDouble(o, (d) => (uint)d, (b) => b, uint.MaxValue, uint.MinValue);
            //型がshort型又はshort?型である場合にはオブジェクトをshort型の値に変換し、返す
            else if (pType == typeof(short) || pType == typeof(short?))
                return GetValueSmallerDouble(o, (d) => (short)d, (b) => b, short.MaxValue, short.MinValue);
            //型がushort型又はushort?型である場合にはオブジェクトをushort型の値に変換し、返す
            else if (pType == typeof(ushort) || pType == typeof(ushort?))
                return GetValueSmallerDouble(o, (d) => (ushort)d, (b) => b, ushort.MaxValue, ushort.MinValue);
            //型がlong型又はlong?型である場合にはオブジェクトをlong型の値に変換し、返す
            else if (pType == typeof(long) || pType == typeof(long?))
                return GetValueSmallerDouble(o, (d) => (long)d, (b) => b, long.MaxValue, long.MinValue);
            //型がulong型又はulong?型である場合にはオブジェクトをulong型の値に変換し、返す
            else if (pType == typeof(ulong) || pType == typeof(ulong?))
                return GetValueSmallerDouble(o, (d) => (ulong)d, (b) => b, ulong.MaxValue, ulong.MinValue);
            //型がfloat型又はfloat?型である場合にはオブジェクトをfloat型の値に変換し、返す
            else if (pType == typeof(float) || pType == typeof(float?))
                return GetValueSmallerDouble(o, (d) => (float)d, (b) => b, float.MaxValue, float.MinValue);
            //型がdouble型又はdouble?型である場合にはオブジェクトをdouble型の値に変換し、返す
            else if (pType == typeof(double) || pType == typeof(double?))
                return ObjectToDouble(o);
            //型がchar型又はchar?型である場合
            else if (pType == typeof(char) || pType == typeof(char?))
            {
                //オブジェクトがstring型の実体でない場合には例外を投げる
                if (!(o is string))
                    throw new TypeMismatchException();

                //オブジェクトをstring型にキャストする
                string s = (string)o;

                //文字列の長さが1でない場合には例外を投げる
                if (s.Length != 1)
                    throw new ConversionException("too long");

                //文字列の0文字目を返す
                return s[0];
            }
            //型がbool型又はbool?型である場合
            else if (pType == typeof(bool) || pType == typeof(bool?))
            {
                //オブジェクトがbool型の実体でない場合には例外を投げる
                if (!(o is bool))
                    throw new TypeMismatchException();

                //オブジェクトをbool型にキャストし、返す
                return (bool)o;
            }
            //型がstring型である場合
            else if (pType == typeof(string))
            {
                //オブジェクトがnullでなく、string型の実体でもない場合には例外を投げる
                if (o != null && !(o is string))
                    throw new TypeMismatchException();

                //オブジェクトをstring型にキャストし、返す
                return (string)o;
            }
            //型がdecimal型又はdecimal?型である場合にはオブジェクトをdecimal型の値に変換し、返す
            else if (pType == typeof(decimal) || pType == typeof(decimal?))
                return (decimal)ObjectToDouble(o);
            else
            {
                //オブジェクトがnullである場合にはnullを返す
                if (o == null)
                    return null;

                //オブジェクトがJSON形式のオブジェクトの型(Dictionary<string, object>)の実体でない場合には例外を投げる
                if (!(o is Dictionary<string, object>))
                    throw new TypeMismatchException();

                //JSON形式のオブジェクトを与えられた型の実体に変換する
                return MapObject(o as Dictionary<string, object>, pType);
            }
        }

次に、JSON形式のオブジェクトを与えられた型の実体に変換する関数を追加します。この型は引数なしのコンストラクタを持っていなければなりません。この型のJsonProperty属性が付与されている全てのプロパティを取得し、それに対応するJSON形式のオブジェクトの要素を取得し、その要素をプロパティの型の実体に変換し、プロパティに設定します。プロパティの型が配列である場合には配列の要素を1つずつ変換しなければなりません。

        //JSON形式のオブジェクトを与えられた型の実体に変換し、返す
        private static object MapObject(Dictionary<string, object> osDict, Type type)
        {
            //与えられた型の実体
            object entity = null;
            try
            {
                //与えられた型の実体を作成する
                entity = Activator.CreateInstance(type);
            }
            //引数なしの構築子がない型である場合には例外を投げる
            catch (MissingMethodException)
            {
                throw new NotSupportedException("only supported type with no parameter constructer");
            }

            //JsonProperty属性が付与されている全てのプロパティを取得する
            foreach (var attr in type.GetProperties()
                .SelectMany((elem) => (JsonPropertyAttribute[])elem.GetCustomAttributes(typeof(JsonPropertyAttribute), false), (elem, attr) => new { PropertyInfo = elem, Name = attr.Name.GetOrDefault(elem.Name) }))
            {
                //JSON形式のオブジェクトが対応する名称のオブジェクトを含まない場合には例外を投げる
                if (!osDict.Keys.Contains(attr.Name))
                    throw new NotFoundException("not found json key", attr.Name);

                //対応する名称のオブジェクトを取得する
                object o = osDict[attr.Name];

                //プロパティの情報を取得する
                PropertyInfo pInfo = attr.PropertyInfo;
                //プロパティの型を取得する
                Type pType = pInfo.PropertyType;

                //プロパティの型が配列である場合
                if (pType.IsArray)
                {
                    //配列が1次元配列でない場合には例外を投げる
                    if (pType.GetArrayRank() != 1)
                        throw new NotSupportedException();

                    //オブジェクトがnullである場合にはプロパティにnullを設定する
                    if (o == null)
                    {
                        pInfo.SetValue(entity, null);

                        continue;
                    }

                    //オブジェクトがobject[]型の実体でない場合には例外を投げる
                    if (!(o is object[]))
                        throw new TypeMismatchException();

                    //配列の要素の型を取得する
                    Type aType = pType.GetElementType();

                    //オブジェクトをobject[]型にキャストする
                    object[] osIn = (object[])o;
                    //配列を作成する
                    Array osOut = Array.CreateInstance(aType, osIn.Length);

                    //配列の要素にオブジェクトの要素を変換した結果を設定する
                    for (int i = 0; i < osIn.Length; i++)
                        osOut.SetValue(GetValue(osIn[i], aType), i);

                    //プロパティに配列を設定する
                    pInfo.SetValue(entity, osOut);
                }
                //プロパティの型が配列でない場合
                else
                    //プロパティにオブジェクトを変換した結果を設定する
                    pInfo.SetValue(entity, GetValue(o, pType));
            }

            //与えられた型の実体を返す
            return entity;
        }

最後に、JSON形式のオブジェクトを与えられた型の実体に変換する公開関数を追加します。ただし、型は型引数として受け取ることにします。

        //JSON形式のオブジェクトを与えられた型の実体に変換し、返す
        public static T Map<T>(Dictionary<string, object> osDict) where T : class
        {
            return MapObject(osDict, typeof(T)) as T;
        }

これで、最初に示したような使い方ができるようになりました。

16
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
16