以前C#でJSONの構文解析器を作成しました。
そして、構文解析器によって得られたJSON形式のオブジェクトを具体的な型の実体に変換するコードも書きました。
今日はこのコードを改良します(。╹ω╹。)。
GitHub
前回、前々回で作成したコードと併せて、GitHubでコード全体を公開しています。
前回のコードの不便な点
前回のコードで次のような使い方ができるようになりました。これはJSONの構文解析器によって得られたオブジェクトをstring
型、int?
型、bool
型、string[]
型、Person
型のプロパティを含む具体的な型の実体に変換するものです。
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);
}
これは非常に便利ではあるものの1つ非常に不便な点があります。それは
Option<T>型に対応していない
ということです。
最近のC#プログラマーなら当然Option<T>型を使いこなしているに違いありませんので、これは大きな欠点になり得ます。今日は主にこの点を解決したいと思います。
とは言え、ただ単にOption<T>型に対応するだけでは面白くありません。今後また「○○型に対応していない」という不満が出てくるとも限りません。そこで、直接Option<T>型に対応するのではなく、まず様々な型に対応するための汎用的な枠組みを追加して、それを使ってOption<T>型に対応することにしましょう。
あと、公開プロパティにしか値を設定できないのも不便なのでこの点も併せて解決しましょう。
それから、自作のOption<T>型を改良したのでこの改良も取り込むことにしましょう。
どうすれば良いのか
前回のコードで基本的な型への対応は出来ていました。そして、それ以外の型というのは、言わば特殊な型なのであって何らかの形で基本的な型から変換できるものです。たとえば、Option<T>型はT型やT?型から変換できます。
そのため、基本的な型から特殊な型に変換するための機構を追加してやれば良いということです。そのような機構には次のような要素が必要です。
- 具体的な型のプロパティの型が特殊な型であるか確認する(たとえば、上記の
Person
型のそれぞれのプロパティの型がOption<T>型であるか確認する)。 - 特殊な型の値がどのような基本的な型の値から変換されるかを指定する(たとえば、Option<T>型の値はTが値型である場合にはT?型の値から変換され、参照型である場合にはT型の値から変換される)。
- 特殊な型の値を基本的な型の値から変換することによって得る(たとえば、Option<T>型の値は基本的な型の値が
null
である場合にはNone<T>型の値となり、そうでない場合にはSome<T>型の値となる。ただし、Some<T>型の値はそれ自身の値として基本的な型の値を含む)。
実装
早速実装しましょう。
まず上で説明した変換を表すための型が必要です。Conversion
クラスを追加します。CanConvert
プロパティ、TypeConverter
プロパティ及びConverter
プロパティが上の箇条書きで列挙したそれぞれの要素に対応します。
//変換クラス
//変換の内容を表すクラス
public class Conversion
{
public Conversion(Func<Type, bool> _canConvert, Func<Type, Type> _typeConverter, Func<object, Type, object> _converter)
{
CanConvert = _canConvert;
TypeConverter = _typeConverter;
Converter = _converter;
}
//引数として受け取った変換先の型がこの変換クラスで対応している型であるかを返す関数
public Func<Type, bool> CanConvert { get; private set; }
//引数として変換先の型を受け取り、変換元の型を返す関数
public Func<Type, Type> TypeConverter { get; private set; }
//変換前の値と変換元の型を受け取り、変換後の値を返す関数
public Func<object, Type, object> Converter { get; private set; }
}
GetValue
関数を変更します。MapObject
関数を呼び出す際の引数が変換クラスの列挙を渡さなければならないため1つ増え、それに伴いこの関数自身の引数も1つ増えたというだけです。影響のない部分は省略しています。変更点は2行目の引数リストと最後から3行目の引数リストだけです。
//オブジェクトを与えられた型の実体に変換し、返す
private static object GetValue(object o, Type pType, IEnumerable<Conversion> conversions)
{
//型がNullable<T>型であり、オブジェクトがnullである場合にはnullを返す
if (pType.IsGenericType && pType.GetGenericTypeDefinition() == typeof(Nullable<>) && o == null)
return null;
(省略)
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, conversions);
}
}
MapObject
関数を変更します。IEnumerable<Conversion>
型の引数を追加します。この引数で変換に必要な情報を渡します。JsonProperty
属性が付与されている全てのプロパティをGetProperties
メソッドで取得する際にはBindingFlags.NonPublic
を指定して非公開プロパティも取得対象とします。JSON形式のオブジェクトから取得した値の変換はMapValue
という新しく作成する関数に任せます。この関数にはJSON形式のオブジェクトから取得した値とプロパティの型と変換クラスの列挙を渡します。この関数によって得られた変換済の値を具体的な型の実体のプロパティに設定します。
//JSON形式のオブジェクトを与えられた型の実体に変換し、返す
private static object MapObject(Dictionary<string, object> osDict, Type type, IEnumerable<Conversion> conversions)
{
//与えられた型の実体
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(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance)
.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);
//プロパティに対応する名称のオブジェクトを変換した結果を設定する
attr.PropertyInfo.SetValue(entity, MapValue(osDict[attr.Name], attr.PropertyInfo.PropertyType, conversions));
}
//与えられた型の実体を返す
return entity;
}
次にMapValue
関数を追加しますが、その前にMapValue
関数で使用するため次のような拡張メソッドと静的メソッドを定義します。
//共通の拡張関数
public static class CommonExtension
{
//列挙の中で述語を充足する最初の要素を随意型の実体として返す
//ただし、そのような要素が存在しない場合にはNone<T>型の実体を返す
public static IOption<T> FirstOption<T>(this IEnumerable<T> self, Func<T, bool> predicate)
{
//列挙の中で述語を充足する最初の要素を取得する
T first = self.FirstOrDefault(predicate);
//nullである場合にはNone<T>型の実体を返す
if (first == null)
return Option.Return<T>();
//そうでない場合にはSome<T>型の実体を返す
else
return Option.Return(first);
}
}
//共通の関数
public static class _
{
//恒等関数
public static Func<T, T> Identity<T>()
{
return (elem) => elem;
}
}
MapValue
関数を追加します。引数はそれぞれオブジェクトの値と変換先の型と変換クラスの列挙です。この関数の処理は変換先の型が配列型である場合の処理とそうでない場合の処理に両分されます。
変換先の型が配列型である場合には、最初に配列型の次元が1であるか確認します。JSONに多次元配列はありませんので(配列の配列はあります)、次元が1以外である場合にはNotSupportedException
を投げます。次に配列型の値はnull
でも良いため、オブジェクトの値がnull
である場合にはそのままnull
を返します。それ以外の場合には、オブジェクトの型はJSON形式のオブジェクトの配列の型、すなわち、object[]
型でなければなりません。そうでない場合には、TypeMismatchException
を投げます。その後は配列のそれぞれの要素に対して変換を行います。配列のそれぞれの要素に対してMapValue
関数を再帰呼び出しすることになります。全ての要素に対する変換が終わったら変換後の値を返します。
変換先の型が配列型でない場合には、変換先の型に対応している変換クラスが存在するか調べます。変換先の型に対応している変換クラスが複数存在する可能性もありますが、一番最初に見付かった変換クラスを使うことにします。変換クラスが見付かった場合には、更に変換クラスの変換元の型を変換先の型とするような変換クラスがないかも調べます。この反復は変換クラスが見付からなくなるまで続きます。ただし、変換元の型が配列型である場合はそこで反復を終わります。これによって変換クラスの系列を作り出します。変換クラスの系列における最後の変換元の型が配列型である場合には、MapValue
関数を再帰呼び出ししてその配列型の値を取得します。そうでない場合には、GetValue
関数を呼び出して値を取得します。最後に、変換クラスの系列を使って取得した値を変換し、変換済の値を返します。
//JSON形式の値を与えられた型の実体に変換し、返す
private static object MapValue(object o, Type type, IEnumerable<Conversion> conversions)
{
//変換先の型が配列型である場合
if (type.IsArray)
{
//配列型の次元が1でない場合には例外を投げる
if (type.GetArrayRank() != 1)
throw new NotSupportedException("multidimensional array is not supported");
//オブジェクトがnullである場合にはnullを返す
if (o == null)
return o;
//オブジェクトがobject[]型の実体でない場合には例外を投げる
if (!(o is object[]))
throw new TypeMismatchException();
//配列の要素の型を取得する
Type aType = type.GetElementType();
//オブジェクトをobject[]型にキャストする
object[] osIn = (object[])o;
//配列を作成する
Array osOut = Array.CreateInstance(aType, osIn.Length);
//配列の要素にオブジェクトの要素を変換した結果を設定する
for (int i = 0; i < osIn.Length; i++)
osOut.SetValue(MapValue(osIn[i], aType, conversions), i);
//配列を返す
return osOut;
}
//変換先の型が配列型でない場合
else
{
//変換クラスと変換元の型の系列
Stack<Tuple<IOption<Conversion>, Type>> conversionsStack = new Stack<Tuple<IOption<Conversion>, Type>>();
//変換元の型
Type cType = type;
//変換前の値
object co = null;
//変換が提供されていないか調べる
while (true)
{
//変換先の型に対応する変換クラスを取得する
IOption<Conversion> conversion = conversions.FirstOption((elem) => elem.CanConvert(cType));
//変換元の型を取得する
Type nextType = conversion.Map((elem) => elem.TypeConverter).GetOrDefault(_.Identity<Type>())(cType);
//これ以上の変換がない場合には変換元の型は確定である
if (cType == nextType)
break;
//そうでない場合には変換クラスと変換元の型を系列に追加し、更なる変換が提供されていないか調べる
else
{
conversionsStack.Push(Tuple.Create(conversion, nextType));
cType = nextType;
//変換元の型が配列型である場合には変換元の型は確定である
if (cType.IsArray)
break;
}
}
//変換元の型が配列型である場合には再帰して変換前の値を取得する
if (cType.IsArray)
co = MapValue(o, cType, conversions);
//そうでない場合にはそのまま変換前の値を取得する
else
co = GetValue(o, cType, conversions);
//変換前の値を変換の系列で変換したものを返す
return conversionsStack.Select((conv) => conv.Item1.Map<Conversion, Func<object, object>>((elem) => (oc) => elem.Converter(oc, conv.Item2)).GetOrDefault(_.Identity<object>())).Aggregate(co, (acc, elem) => elem(acc));
}
}
最後に変換クラスの列挙を引数として受け取る新しいMap
関数を追加します。外部からはこの関数を呼び出すことになります。
//JSON形式のオブジェクトを与えられた型の実体に変換し、返す
public static T Map<T>(Dictionary<string, object> osDict, IEnumerable<Conversion> conversions) where T : class
{
return MapObject(osDict, typeof(T), conversions) as T;
}
使用例1
Option<T>型のプロパティに対応してみましょう。
最初に、Option<T>型のための変換クラスの実体を追加します。
//変換クラスを提供するクラス
public static class JsonMapperConversions
{
static JsonMapperConversions()
{
//随意型のための変換クラスの実体を作成する
OptionConversion = new Conversion(
//引数として受け取った変換先の型がこの変換クラスで対応している型であるかを返す関数
//Option<T>型である場合には変換可能である
_canConvert: (type) => type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IOption<>),
//引数として変換先の型を受け取り、変換元の型を返す関数
_typeConverter: (type) =>
{
try
{
//Option<T>型からNullable<T>型に変換する
//値型の場合には成功するが、参照型の場合には失敗する(ArgumentExceptionが発生する)
return typeof(Nullable<>).MakeGenericType(type.GetGenericArguments());
}
catch (ArgumentException)
{
//ArgumentExceptionが発生した場合には参照型なので、
//Option<T>型からT型に変換する
return type.GetGenericArguments()[0];
}
},
//変換前の値と変換元の型を受け取り、変換後の値を返す関数
_converter: (o, type) =>
{
//変換元の型の型引数を取得する
//変換先の型の型引数とする
Type[] genericArguments = type.GetGenericArguments();
//変換元の型の型引数が0個である場合には変換先の型の型引数を変換元の型とする
if (genericArguments.Length == 0)
genericArguments = new Type[] { type };
//変換前の値がnullである場合にはNone<T>型の実体を返す
if (o == null)
return Activator.CreateInstance(typeof(None<>).MakeGenericType(genericArguments));
//そうでない場合にはSome<T>型の実体を返す
//ただし、実体自身の値として変換前の値を指定する
else
return Activator.CreateInstance(typeof(Some<>).MakeGenericType(genericArguments), o);
}
);
}
//随意型のための変換クラスの実体
public static readonly Conversion OptionConversion;
}
これを使って、JSON形式のオブジェクトから具体的な型の実体への変換を試してみます。
public class Person2
{
[JsonProperty("name")]
public string Name { get; private set; }
[JsonProperty("age")]
public IOption<int> Age { get; private set; }
[JsonProperty("isMale")]
public bool IsMale { get; private set; }
[JsonProperty("hobby")]
public string[] Hobby { get; private set; }
[JsonProperty("father")]
public IOption<Person2> Father { get; private set; }
}
public class Persons2
{
[JsonProperty("persons")]
public IOption<Person2[]> 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\": null, \"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>;
Persons2 ps = JsonMapper.Map<Persons2>(osDict, new Conversion[] { JsonMapperConversions.OptionConversion });
}
上手く変換できているはずです。
使用例2
日時型のプロパティに対応してみましょう。"yyyy-MM-dd"
のような形式の文字列を日時型に変換します。
最初に、日時型のための変換クラスの実体を追加します。日時型は値型であるためDateTime
型とDateTime?
型の両方に対応しなければなりません。
//変換クラスを提供するクラス
public static class JsonMapperConversions
{
static JsonMapperConversions()
{
(省略)
//日時型のための変換クラスの実体を作成する
DateTimeConversion = new Conversion(
//引数として受け取った変換先の型がこの変換クラスで対応している型であるかを返す関数
//DateTime型である場合には変換可能である
_canConvert: (type) => type == typeof(DateTime),
//引数として変換先の型を受け取り、変換元の型を返す関数
//string型を返す
_typeConverter: (type) => typeof(string),
//変換前の値と変換元の型を受け取り、変換後の値を返す関数
//文字列を日時型に変換し、返す
_converter: (o, type) => DateTime.Parse((string)o)
);
//ヌル許容日時型のための変換クラスの実体を作成する
DateTimeNullableConversion = new Conversion(
//引数として受け取った変換先の型がこの変換クラスで対応している型であるかを返す関数
//DateTime型である場合には変換可能である
_canConvert: (type) => type == typeof(DateTime?),
//引数として変換先の型を受け取り、変換元の型を返す関数
//string型を返す
_typeConverter: (type) => typeof(string),
//変換前の値と変換元の型を受け取り、変換後の値を返す関数
//文字列を日時型に変換し、返す
_converter: (o, type) => o == null ? null : (DateTime?)DateTime.Parse((string)o)
);
}
//随意型のための変換クラスの実体
public static readonly Conversion OptionConversion;
//日時型のための変換クラスの実体
public static readonly Conversion DateTimeConversion;
//ヌル許容日時型のための変換クラスの実体
public static readonly Conversion DateTimeNullableConversion;
}
これを使って、JSON形式のオブジェクトから具体的な型の実体への変換を試してみます。ついでに、IOption<string[]>[]
のような若干複雑な型の場合も試してみましょう。
public class Todo
{
[JsonProperty("begin")]
public IOption<DateTime> Begin { get; private set; }
[JsonProperty("todos")]
public IOption<string[]>[] Todos { get; private set; }
}
public static void Main(string[] args)
{
string json = "{\"begin\": \"2015-11-01\", \"todos\": [null, [\"trading\", \"math\", \"data analysis\"], null, []]}";
Dictionary<string, object> osDict = JsonParser.Parse(new Scanner(new Text(json))) as Dictionary<string, object>;
Todo ps = JsonMapper.Map<Todo>(osDict, new Conversion[] { JsonMapperConversions.OptionConversion, JsonMapperConversions.DateTimeConversion, JsonMapperConversions.DateTimeNullableConversion });
}
上手く変換できているはずです。
使用例3
List<T>型のプロパティに対応してみましょう。
最初に、List<T>型のための変換クラスの実体を追加します。
//変換クラスを提供するクラス
public static class JsonMapperConversions
{
static JsonMapperConversions()
{
(省略)
//リスト型のための変換クラスの実体を作成する
ListConversion = new Conversion(
//引数として受け取った変換先の型がこの変換クラスで対応している型であるかを返す関数
//List<T>型である場合には変換可能である
_canConvert: (type) => type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>),
//引数として変換先の型を受け取り、変換元の型を返す関数
_typeConverter: (type) =>
{
//List<T>型からT[]型に変換する
return type.GetGenericArguments()[0].MakeArrayType();
},
//変換前の値と変換元の型を受け取り、変換後の値を返す関数
_converter: (o, type) =>
{
//変換元の型の要素の型を取得する
//変換先の型の型引数とする
Type[] genericArguments = new Type[] { type.GetElementType() };
//変換前の値がnullである場合にはnullを返す
if (o == null)
return null;
//そうでない場合にはList<T>型の実体を返す
//ただし、実体の要素として変換前の配列の要素を指定する
else
return Activator.CreateInstance(typeof(List<>).MakeGenericType(genericArguments), o);
}
);
}
//随意型のための変換クラスの実体
public static readonly Conversion OptionConversion;
//日時型のための変換クラスの実体
public static readonly Conversion DateTimeConversion;
//ヌル許容日時型のための変換クラスの実体
public static readonly Conversion DateTimeNullableConversion;
//リスト型のための変換クラスの実体
public static readonly Conversion ListConversion;
}
これを使って、JSON形式のオブジェクトから具体的な型の実体への変換を試してみます。
public class Seiyuus
{
[JsonProperty("wakai")]
public List<string> Wakai { get; private set; }
[JsonProperty("wakakunai")]
public List<string> Wakakunai { get; private set; }
}
public static void Main(string[] args)
{
string json = "{\"wakai\": [\"小倉唯\", \"石原夏織\", \"雨宮天\", \"小澤亜李\", \"伊藤美来\", \"水瀬いのり\"], \"wakakunai\": null}";
Dictionary<string, object> osDict = JsonParser.Parse(new Scanner(new Text(json))) as Dictionary<string, object>;
Seiyuus ps = JsonMapper.Map<Seiyuus>(osDict, new Conversion[] { JsonMapperConversions.ListConversion });
}
上手く変換できているはずです。