NewtonsoftさんのJson.NET、大変便利ですよね。
MITライセンスということで.NET向けの各種SNSのAPIクライアントライブラリなどを中心に様々な場所で利用されております (2018年02月26日 現在)。

Json.NETでinterfaceのデシリアライズを出来るようにしてみようというお話です。

Json.NET


イメージ 1
図1 NuGetで配布されているJson.NET

.NET Framework向けのオープンソースのJSONパースライブラリとして、NuGet等でMITライセンスで配布されております。
.NETに標準で実装されているXMLやSoapXML向けのXmlSerializer、SoapFormatter同様、オブジェクト インスタンスをJSONへシリアライズする機能、JSONから定義済みのクラスを元にオブジェクト インスタンスを生成する機能、この他JSONを.NET上で取り扱うための様々な機能が実装されております。

え? .NET標準ライブラリにもJSON用のシリアライザがあるですって??
いえ、知らないクラスですね…

本記事で取り扱う内容

Json.NETでは、上記で述べた通りJSONへのシリアライズ、JSONからのデシリアライズ機能がございますが、このうちデシリアライズには制約がございます。
  • 型が具体的に指定されていること
  • あるいは、抽象的に指定された型 (interfaceやabstract class) に対する JsonConverter が定義されていること
デシリアライズを行う際、Json.NET はすでに定義されている型の情報を元にJSONを読み込んでインスタンスを生成します。
インスタンスの生成が直接できない型 (例えばinterfaceなど) が指定されている場合、シリアライズを行うことが出来てもデシリアライズには失敗してしまいます。

イメージ 2
図2 interface 型のデシリアライズの例外

Newtonsoft.Json.JsonSerializationException: 'Could not create an instance of type Sample.TestClasses.ITestClass. Type is an interface or abstract class and cannot be instantiated. Path '', line 1, position 2.'

この例外は、対象が具体的な型であっても、型の内部に抽象的な型で宣言されているフィールド (プロパティ)などが含まれていれば発生します。
上記のような例外が発生したのに、型自体は具体的である場合、その型のフィールド (プロパティ)に interface 型で宣言されたフィールド (プロパティ)が存在している可能性があります。ご確認ください。

interfaceで型指定が行われているものに対してデシリアライズを行う場合、次の方法で問題を解決します。

  1. interface 内に JsonConverter がデシリアライズ先を判断するためのヒントとなるフィールド (プロパティ)を定義
  2. デシリアライズに対応させたいインタフェースに対する JsonConverter を定義
  3. interface の定義に対して、上記で定義した JsonConverter を利用させるための属性の付与

サンプルプログラムについて

本記事で取り上げるプログラムは次のような構成です。

  • Sample.Program クラス
  • Sample.TestClasses.ITestClass インターフェイス
  • Sample.TestClasses.Alpha クラス
  • Sample.TestClasses.Beta クラス

あ、ちなみに次のURLで配布中。
ご自由にお使い下さい。
a32kita/JsonInterfaceDeserializeSample
https://github.com/a32kita/JsonInterfaceDeserializeSample

以上のうち「Alpha」「Beta」は「ITestClass」を実装しており、JsonConverter を定義することで、任意のITestClass実装型からシリアライズされたJSONデータからITestClass型へのデシリアライズを実現することを目的とします。

イメージ 3
図3 サンプルプログラムの言語バージョン

なお、本記事で取り上げるサンプルプログラムは、C# 6.0の記法をがっつり使っております。
C# 6.0が使えない環境下でも対して影響は無いかと思いますが、その辺は適宜ビルドエラーを見ながら書き換えをしていただければなと思います。

ちなみに最初の状態は次の通り。
・・・と思ったのですが、文字数制限の都合上、載せられませんでした。
上記リンクからサンプルコードをご覧ください。

この状態ですと、Mainメソッド内のSerializeObject呼び出し部は通常通り実行可能ですが、DeserializeObject で先に紹介した図1のような例外が発生してしまいます。

interface内に型の判断材料となるフィールド (プロパティ)を設置


イメージ 4
図4 JsonConverterがJSONデータから型を判断するイメージ

JSONデータからデシリアライズ処理でインスタンスを生成しますが、この後用意する JsonConverter を通してデシリアライズ処理が実施されるように変更します。
その際、JsonConverterがJSONデータを見て、シリアライズ前は何型であったのかを判断できるようにならなくてはなりません。

イメージ 5
図5 定義したフィールド (プロパティ)

インターフェイスにこのフィールド (プロパティ)を定義したので、もちろんインターフェイスを実装しているクラス (ここでは「Alpha」、「Beta」) にもフィールド (プロパティ)を定義しなくてはなりません。
また、型の判断情報となる値を代入させる必要があります。
このサンプルでは、クラス名をこのフィールド (プロパティ)に格納しておきます。

イメージ 6
図6 型名の格納

別にフィールド (プロパティ)という形でなくても、とにかくJsonConverterが具体的な型を判断する手立てがあれば構いません。


JsonConverter「TestClassConverter」の定義

JsonConverterを定義していきます。
JsonConverterは、Json.NETから提供されるJsonConverterクラスを継承する形で定義します。

実装のお話はサンプルコードをご覧頂いたほうが早そうですね。

さてさて、まずは継承から。Newtonsoft.Json.JsonConverter<T> を継承します。T は、ここでは ITestClass ということになります。
継承しただけなのに早速怒られますが、まずはサクッと NotImplementedException のメソッドを置いてとりあえず黙らせておきましょう。

イメージ 7
図7 継承エラー

まず2つのプロパティ、CanRead、CanWriteのオーバーライドを実装します。CanReadはtrue、CanWriteはfalseを返すようにすれば十分です。
コンバータではJSONの読み取りのみ対応していることを示すためです。この場合、JSONの書き込みの際は標準の処理が実行されます。

次に先程NotImplementedExceptionをぶん投げるだけにしておいたReadJsonとWriteJson。
このうちWriteJsonは、CanWriteでfalseを示すようにしたので、実行されることがなく、NotImplementedExceptionのままで問題ありません。

ReadJsonメソッドは、ざっくり次のような感じ。
public override ITestClass ReadJson(JsonReader reader, Type objectType, ITestClass existingValue, bool hasExistingValue, JsonSerializer serializer)
{
  // JSONの中身を読めるようにする
  var tokens = Newtonsoft.Json.Linq.JToken.ReadFrom(reader);

  // TargetTypeの値を取得
  var targetType = tokens["TargetType"].ToString();

  // 型判断
  switch (targetType)
  {
    case nameof(Alpha):
      return tokens.ToObject<Alpha>();
    case nameof(Beta):
      return tokens.ToObject<Beta>();
    default:
      throw new NotSupportedException($"型 {targetType} は {nameof(TestClassConverter)} ではサポートしておりません。");
  }
}


先にTargetTypeの値だけ取得して、そこから判断して具体的なシリアライズ先の型を決定しているだけです。簡単ですね。

あとは、このJsonConverterをITestClass型のデシリアライズ時に利用させるための属性を付与します。

イメージ 8
図8 属性の付与

これで問題なくITestClassのデシリアライズが為されるはず…であった、
がしかしこの後とんでもない衝撃の事実が我らを待ち受けるのです…

StackOverflowExceptionの回避

実は、先程定義した JsonConverter だけでは不十分なんです。
実行したとき次のような問題が、

イメージ 9
図9 StackOverflowException

先程定義した「TestClassConverter」のWriteJsonメソッドの中にブレークポイントを設置したら、何回も繰り返し呼ばれていることがわかりました。
と、言いますのも先程のプログラムのうち「tokens.ToObject<Alpha>();」がどうも悪さをしているようで。。
詳しい原因は調査していないのですが、ITestClassに置いたJsonConverterの属性がAlphaクラスにも適用されるとJsonSerializer側からみなされているようで、Alphaのデシリアライズ時もITestClassのデシリアライズ時同様、TestClassConverterが呼ばれちゃってるみたいなんですよね。

この問題を回避する方法はいくつかあるのですが、今回は一番わかり易い方法で行こうと思います。
回避法はズバリ「何もしないConverterを定義し、「Alpha」クラスと「Beta」クラスへ適用させてしまえというもの。
受け継がれし属性を幻想殺し(読み「イマジンブレイカー」)してしまおうという強引技です。

イメージ 10
図10 イマジンブレイカー

イメージ 11
図11 クラスへの適用

コードはサンプルプログラムを見てください。
これでやっと本当に完成です。
お疲れ様でした。

イメージ 12
図12 実行結果

珍しく長編となってしまいました。
もしかすると、もしかしなくても、もっと頭の良い方法を使っている方もいらっしゃるかもしれません。

また型名をデシリアライズ時のヒントにする際の注意点なんですが、Activatorなんかを利用してJSONデータの中に書いてあった型名をインスタンス化するような方法は避けて下さい。
JSONデータがネットを彷徨っている間に悪意のある人間に書き換えられてしまった、なんてことがあるととんでもないセキュリティホールになりかねないので。。。
型のヒントの部分に関しては、飽くまでも簡単に説明するために型名を利用しましたが、そこは皆さんのお作りになっているソフトウェアに応じて書き換えてください。

最後に

本記事で取り上げたサンプルプログラムについて、筆者 (あおと) が著作権を主張することはございません。
これ使える、と思ったらご自由にお使いになって下さい。

なお、Json.NET (Newtonsoft.Json) は、Newtonsoft様がMIT Licenseで頒布しております。
詳しくは、Newtonsoft様のウェブサイトをご覧ください。

では。