DartのパターンマッチでJSONを扱う

Dart 3で導入されたパターンマッチに慣れるため、JSONのパースを試してみたところ、とても便利で最高だったのでブログに残しておきたい。

パターンマッチを使わない場合

const pokedex = '''[
  {"id": 1, "name": "Bulbasaur", "type": ["Grass", "Poison"]},
  {"id": 4, "name": "Charmander", "type": ["Fire"]},
  {"id": 7, "name": "Squirtle", "type": ["Water"]}
]''';

void main() {
  try {
    final array = json.decode(pokedex);
    if (array is List<dynamic>) {
      for (final object in array) {
        if (object is Map<String, dynamic> &&
            object['id'] is int &&
            object['name'] is String &&
            object['type'] is List<dynamic>) {
          print(
              'id: ${object['id']}, name: ${object['name']}, types: ${object['type']}');
        }
      }
    }
  } catch (_) {
    print('failed to decode');
  }
}

要素の型をちゃんとチェックしようとすると、かなり冗長なコードになってしまう。

パターンマッチを使う場合

const pokedex = '''
[
  {"id": 1, "name": "Bulbasaur", "type": ["Grass", "Poison"]},
  {"id": 4, "name": "Charmander", "type": ["Fire"]},
  {"id": 7, "name": "Squirtle", "type": ["Water"]}
]
''';

void main() {
  if (json.decode(pokedex) case List<dynamic> array) {
    for (final {
          'id': int id,
          'name': String name,
          'type': List<dynamic> types,
        } in array) {
      print('id: $id, name: $name, types: $types');
    }
  } else {
    print('failed to decode');
  }
}

型チェックと同時に変数へのbindをおこなっているため、簡潔でわかりやすいコードになった。Mapの要素に対する型チェックもかなり簡潔になっている。パターンマッチのポイントは以下のとおり。

  • if (<変数> case <パターン>) {}でパターンマッチをおこない、マッチしたかどうかで分岐できる。
  • for (<パターン> in <変数>) {}でリストの要素に対してパターンマッチをおこない、マッチした要素だけを処理できる。
  • パターンに{'<キー>': <パターン>}を渡すことでキーを指定しつつ、値に対してさらなるパターンマッチをおこなえる。
  • int idのように型宣言した変数名をパターンに渡すことで、型に一致した場合のみマッチするようにしつつ、変数名にマッチした値をbindさせている。
  • List<dynamic> typesとしているところをList<String> typesとすると、Uncaught Error: Bad state: Pattern matching errorというエラーが発生してしまうので、dynamicのままにしている。ここはもっとうまいやり方があるかも。