Dartのjson_serializableの生成のバリエーション

by

@wapa5pow

json_serializableはDartのインスタンスをJSONにしたり、JSONからインスタンスに変換してくれるメソッドを自動ではやしてくれます。
自動ではやしてくれるのはいいのですがその生成結果が様々あってすぐに理解するのが難しいです。

ここではjson_serializableがどのようなクラス定義でどのようなJSONを出すのか見ていきたいと思います。
(提示されているコードは長くならないように一部省略してあります)

フィールドが何もないクラス

// クラス定義
()
class Person {
  Person();

  factory Person.fromJson(Map<String, dynamic> json) => _$PersonFromJson(json);

  Map<String, dynamic> toJson() => _$PersonToJson(this);
}

// 生成されたクラス
Person _$PersonFromJson(Map<String, dynamic> json) => Person();

Map<String, dynamic> _$PersonToJson(Person instance) => <String, dynamic>{};

何もないのですっきりしています。

初期値があり変更可能なnameを定義

()
class Person {
  String name = '';

  Person();

  factory Person.fromJson(Map<String, dynamic> json) => _$PersonFromJson(json);

  Map<String, dynamic> toJson() => _$PersonToJson(this);
}

Person _$PersonFromJson(Map<String, dynamic> json) =>
    Person()..name = json['name'] as String;

Map<String, dynamic> _$PersonToJson(Person instance) => <String, dynamic>{
      'name': instance.name,
    };

Personのコンストラクタがないと生成できないので入れました。
json_serializableはPersonをコンストラクタで生成してから読み込んだnameを代入しています。
jsonにnameがないと例外が発生します。

初期値を設定し例外を発生しないようにする

()
class Person {
  (defaultValue: '')
  String name = '';

  Person();

  factory Person.fromJson(Map<String, dynamic> json) => _$PersonFromJson(json);

  Map<String, dynamic> toJson() => _$PersonToJson(this);
}

Person _$PersonFromJson(Map<String, dynamic> json) =>
    Person()..name = json['name'] as String? ?? '';

Map<String, dynamic> _$PersonToJson(Person instance) => <String, dynamic>{
      'name': instance.name,
    };

@JsonKey(defaultValue: '')を設定すれば初期値が設定してあるのでjsonにnameがなくても例外が発生しません。

なお初期値は以下のようにコンストラクタで設定することもできます

()
class Person {
  String name;

  Person({
    this.name = '',
  });

  Person.red() : name = 'red';

  factory Person.fromJson(Map<String, dynamic> json) => _$PersonFromJson(json);

  Map<String, dynamic> toJson() => _$PersonToJson(this);
}

Person _$PersonFromJson(Map<String, dynamic> json) {
  final v = Person(
    name: json['name'] as String? ?? '',
  );
  return v;
}

Map<String, dynamic> _$PersonToJson(Person instance) => <String, dynamic>{
      'name': instance.name,
    };

Immutableにして生成後変更できないようにする

()
class Person {
  final String name;

  Person({required this.name});

  factory Person.fromJson(Map<String, dynamic> json) => _$PersonFromJson(json);

  Map<String, dynamic> toJson() => _$PersonToJson(this);
}

Person _$PersonFromJson(Map<String, dynamic> json) => Person(
      name: json['name'] as String,
    );

Map<String, dynamic> _$PersonToJson(Person instance) => <String, dynamic>{
      'name': instance.name,
    };

コンストラクタでjsonから読んだnameを設定するようになりました。

mixinのクラスに変数を定義する

mixin Document {
  String id = '';
}

()
class Person with Document {
  (defaultValue: '')
  final String name;

  Person({required this.name});

  factory Person.fromJson(Map<String, dynamic> json) => _$PersonFromJson(json);

  Map<String, dynamic> toJson() => _$PersonToJson(this);
}

Person _$PersonFromJson(Map<String, dynamic> json) => Person(
      name: json['name'] as String? ?? '',
    )..id = json['id'] as String;

Map<String, dynamic> _$PersonToJson(Person instance) => <String, dynamic>{
      'id': instance.id,
      'name': instance.name,
    };

mixinでDocumentにはidが定義されており変更可能なのでPersonを生成してからidをjsonから読んで設定しています。

jsonから生成されたクラスの場合コンストラクタを分ける

コンストラクタ後にvalidationを実行していたとします。jsonを永続化としてデータベースに保存していてその値をfromJsonしようとしたときにvalidationを通したくないときがあります。
たとえばパフォーマンス上の理由によりフィールドの一部のみ取得した場合は不正なデータなのでそのままvalidationを実行したくないパターンです(これ自体はあまりよくない設計ですがいったんそのように考えます)。

その場合はfromJsonで生成されたかどうかのフラグをつければいいので以下のようにfromJsonが呼び出すコンストラクタを変えます。

mixin Document {
  (defaultValue: '')
  String id = '';
}

(constructor: 'forFromJson')
class Person with Document {
  (defaultValue: '')
  final String name;

  Person({
    required this.name,
    bool fromJson = false,
  }) {
    if (fromJson) {
      print('fromJson on');
    }
  }

  factory Person.forFromJson({
    required String name,
  }) =>
      Person(name: name, fromJson: true);

  factory Person.fromJson(Map<String, dynamic> json) => _$PersonFromJson(json);

  Map<String, dynamic> toJson() => _$PersonToJson(this);
}

Person _$PersonFromJson(Map<String, dynamic> json) => Person.forFromJson(
      name: json['name'] as String? ?? '',
    )..id = json['id'] as String? ?? '';

Map<String, dynamic> _$PersonToJson(Person instance) => <String, dynamic>{
      'id': instance.id,
      'name': instance.name,
    };

これでfromJsonのときのみフラグをつけれました。

外部からは変更できないようなクラスを作る

以下のようにすればクラス外からはフィールドの値を変更できないようにできました。
フィールドを変更したい場合はかならずメソッドを呼びます。
配列もUnmodifiableListViewを設定することにより外部から直接変更できないようになっています。

(explicitToJson: true)
class Child {
  String _childName;
  String get childName => _childName;

  Child({
    required String childName,
  }) : _childName = childName;

  factory Child.fromJson(Map<String, dynamic> json) => _$ChildFromJson(json);
  Map<String, dynamic> toJson() => _$ChildToJson(this);
}

(explicitToJson: true)
class Person {
  String _name;
  String get name => _name;

  List<Child> _children;
  List<Child> get children => UnmodifiableListView(_children);

  
  Person({
    required String name,
    required List<Child> children,
  })  : _name = name,
        _children = children;

  Person.red()
      : _name = 'red',
        _children = [];

  factory Person.fromJson(Map<String, dynamic> json) => _$PersonFromJson(json);
  Map<String, dynamic> toJson() => _$PersonToJson(this);

  void addChild(Child child) {
    _children.add(child);
  }
}

Child _$ChildFromJson(Map<String, dynamic> json) {
  final v = Child(
    childName: json['childName'] as String,
  );
  return v;
}

Map<String, dynamic> _$ChildToJson(Child instance) => <String, dynamic>{
      'childName': instance.childName,
    };

Person _$PersonFromJson(Map<String, dynamic> json) {
  final v = Person(
    name: json['name'] as String,
    children: (json['children'] as List<dynamic>)
        .map((e) => Child.fromJson(e as Map<String, dynamic>))
        .toList(),
  );
  return v;
}

Map<String, dynamic> _$PersonToJson(Person instance) => <String, dynamic>{
      'name': instance.name,
      'children': instance.children.map((e) => e.toJson()).toList(),
    };

Enhanced Enums

enum Code {
  success(200),
  movedPermanently(301),
  found(302),
  internalServerError(500);

  const Code(this.code);
  final int code;
}

()
class Person {
  (defaultValue: '')
  final String name;

  final Code code;

  Person({
    required this.name,
    required this.code,
  });

  factory Person.fromJson(Map<String, dynamic> json) => _$PersonFromJson(json);

  Map<String, dynamic> toJson() => _$PersonToJson(this);
}

Person _$PersonFromJson(Map<String, dynamic> json) => Person(
      name: json['name'] as String? ?? '',
      code: $enumDecode(_$CodeEnumMap, json['code']),
    );

Map<String, dynamic> _$PersonToJson(Person instance) => <String, dynamic>{
      'name': instance.name,
      'code': _$CodeEnumMap[instance.code]!,
    };

const _$CodeEnumMap = {
  Code.success: 'success',
  Code.movedPermanently: 'movedPermanently',
  Code.found: 'found',
  Code.internalServerError: 'internalServerError',
};

何もしないと文字列で変換されてしまいますが@JsonEnum(valueField: 'code')をenum Codeにつけると生成物が以下のようになります。

Person _$PersonFromJson(Map<String, dynamic> json) => Person(
      name: json['name'] as String? ?? '',
      code: $enumDecode(_$CodeEnumMap, json['code']),
    );

Map<String, dynamic> _$PersonToJson(Person instance) => <String, dynamic>{
      'name': instance.name,
      'code': _$CodeEnumMap[instance.code]!,
    };

const _$CodeEnumMap = {
  Code.success: 200,
  Code.movedPermanently: 301,
  Code.found: 302,
  Code.internalServerError: 500,
};

まとめ

json_serializableはクラスのフィールドによってどのような値を生成するか見てきました。
やっていることは単純でコンストラクタの引数があればインスタンスを生成してそれ以外のmutableなフィールドは生成後に代入するというものです。
一方でアノテーションのJsonSerializableやJsonKeyを設定するといろいろなバリエーションがつくれません。他にも様々な引数があるので調べてみるとやりたいことがよりやれるようになるかもしれません。