Flutter-JSONのシリアライズ・デシリアライズ

継承関係にあるクラスをJSONでシリアライズ・デシリアライズする場合の方法について。
使うパッケージはjson_serializable。

ググるとシリアライズについてはヒットするのだけど、デシリアライズは見つけることができず、とりあえず自分でやってみた。
実装後、しつこくググったらdart_json_mapperというのでできることが分かり、そちらについては継承関係にあるクラスのデシリアライズの注意点だけ記述しておく。

ここで取り扱ったのは、以下の様なクラス構造のもの。

PlantUML Syntax:</p>
<p>ClassA<|-ClassB</p>
<p>ListClass*-“*”ClassA</p>
<p>

ListClass内でList<ClassA>を保持しているといったケース。

json_serializable

json_serializable | Dart package
Automatically generate code for converting to and from JSON by annotating Dart classes.

多分、標準的なJSONのシリアライズ・デシリアライズのパッケージなのだと思う。

継承関係にあるクラスをデシリアライズする場合に、ちょっと特殊な方法を用いないとできなかった。

json_serializable:A sample that serializes and deserializes inherited classes.
json_serializable:A sample that serializes and deserializes inherited classes. - dart_sample1.dart

サンプルとして作成したソースは上の場所に配置している。

これで作成したクラスをシリアライズした結果は次のようになる。

{"list":[{"title":"hoge","count":5},{"title":"hage","count":5},{"title":"fuga","count":3,"value":2.0,"type":"classb"}]}

解説

継承関係にあるクラスをデシリアライズしたい場合「何のクラスを作成するのか」という情報がないと作れない。
単純にjson_serializableで自動生成されたシリアライズ・デシリアライズ処理を使っただけでは、すべてClassAにデシリアライズされてしまう。

そこで、JSONファイル内に何のクラスを生成するのかというクラスタイプを書き込み、それを元に派生クラスを生成するといった手段を取ることにする。
(この方法自体はdart_json_mapperでも同様だったので、ありふれた方法だとおもう)

シリアライズ

クラスの情報をJSONに書き込むのは以下の様にしている。

Map<String, dynamic> toJson() {
  final result = _$ClassBToJson(this);
  result[ClassA.classType] = className;
  return result;
}

ClassA.classTypeはクラス識別のためのJSONのタグ名で「type」を設定している。classNameはClassBのstatic変数として定義しているクラスの名前。
これでtoJsonで書き込むことで、"type":"classb"という情報が出力されることになる。

デシリアライズ

List<ClassA>という型で読み込もうとしているため、ClassAのデシリアライズで型の名前による生成ファクトリーを切り替える処理が必要になる。

  static Map<String, ClassA Function(Map<String, dynamic>)> factorymap = {};

  factory ClassA.fromJson(Map<String, dynamic> json) {
    if (!json.containsKey(classType)) {
      return _$ClassAFromJson(json);
    } else {
      return factorymap[json[classType] as String]!(json);
    }
  }

これが上の様な実装。

factorymapはクラス名とそれに対応する生成用のファクトリーを保存するマップで、ClassA.fromJson内で、クラスを示すタグが来たらそれに合わせたファクトリを選択、使用するといったものになっている。

当然、上だけの実装では派生クラスを生成できるわけではなく、factorymapに派生クラス側のfromJsonを追加する必要がある。
そちらはClassB側に用意しておき、main処理等で必要とする派生クラスの登録を行わなければいけない。

  static void register() {
    ClassA.factorymap[className] = ClassB.fromJson;
  }

上のがClassB側で用意した登録メソッドで、ClassB.register()を呼び出す。

dart_json_mapper

dart_json_mapper | Dart package
This package allows programmers to annotate Dart objects in order to serialize / deserialize them from / to JSON.

json_serializableでデシリアライズの実装を行った後、ググったら上のパッケージを見つけてしまった。たぶんこれがあるから継承関係にあるクラスのデシリアライズの質問がなかったのかな?

サンプルや実行方法などについてはある程度そろっているのでそちら参照してもらう方が速いかもしれない。

使用上の注意点

ここで記述するのは、継承関係にあるクラスのシリアライズ・デシリアライズに関する注意点。

PlantUML Syntax:</p>
<p>ClassA<|-ClassB</p>
<p>ListClass*-“*”ClassA</p>
<p>

このような関係のクラスでも処理可能かなと思ったのだけど、実装してデシリアライズを行ったら例外が発生してしまった。

PlantUML Syntax:</p>
<p>abstract class base</p>
<p>base<|-ClassA</p>
<p>ClassA<|-ClassB</p>
<p>ListClass*-“*”base</p>
<p>

こんな感じに、abstractなクラスを基点としないとだめだった。

dart_json_mapperページのサンプルとして、クラスの識別としてenumを使っているけど、こちらはenum以外でも行けるようだ。
私が作成したサンプルでStringを使用したが、問題なくデシリアライズできた。

コメント

タイトルとURLをコピーしました