こんにちは!FlutterやDartを使った開発を楽しんでいますか?
他の言語、例えばTypeScriptやRustなどで「パターンマッチング」の便利さを知ってしまうと、「DartでもスマートにJSONをサクサクさばきたい!」って思いますよね。
Dart 3で導入されたパターンマッチングは、単なる「ちょっとしたシンタックスシュガー(書きやすさのための工夫)」ではありません。コンパイラの型推論システムと深く統合され、「実行時まで保証されない危ういデータ(JSONなど)を、安全なドメインモデルへと昇華させるための最強の武器」なんです。
今回は、外部APIから飛んでくるカオスなJSONデータを、Dart 3のパターンマッチングを使ってエレガントかつ堅牢にパースする実務的な実装パターンを一緒に見ていきましょう。ここをクリアすれば、あなたのDartのコードは一段と洗練されますよ!
—
1. なぜJSONパースにパターンマッチングなのか?
外部APIから送られてくる `Map
従来のDartコードでは、こんな風に書いていませんでしたか?
// 昔ながらの泥臭いパース処理
final name = json[‘name’];
if (name is String) {
// 処理…
} else {
throw FormatException(‘Invalid name’);
}
これだと、階層が深くなったり、フィールドの型が揺らぎだしたりした途端に、コードが`if-else`の要塞と化してしまいますよね。
Dart 3の `switch` 式とパターンマッチングを使えば、「データの構造そのものを形として捉え、一撃で安全に取り出す」ことができます。コンパイラが「このパターンが来たらこの型」と完全に追跡してくれるため、非常にスマートでバグの入り込む隙がないコードになります。
—.
2. 実践!JSONパーサーを自作する
今回は、ECサイトのユーザー情報を取得するAPIレスポンスを想定してみましょう。
レスポンスには、通常の「アクティブなユーザー」だけでなく、「ゲストユーザー」や「エラー情報」が混ざって返ってくるという、よくある少し複雑なシチュエーションを考えてみます。
以下のコードを見てください。これがDart 3の真骨頂です。
import ‘dart:convert’;
// 1. アプリ内で扱うドメインモデル(密封クラス:sealed class)
sealed class UserResult {}
class ActiveUser extends UserResult {
final String id;
final String name;
final int age;
ActiveUser({required this.id, required this.name, required this.age});
}
class GuestUser extends UserResult {
final String guestToken;
GuestUser({required this.guestToken});
}
class ApiError extends UserResult {
final String message;
ApiError({required this.message});
}
// 2. JSONをパースしてドメインモデルに変換する関数
UserResult parseUserResponse(String jsonString) {
// まずは安全にJSONデコード
final Object? decoded = jsonDecode(jsonString);
// switch式とパターンマッチングによる構造化分解
return switch (decoded) {
// パターンA: 正常なアクティブユーザーの構造に一致する場合
{
‘status’: ‘success’,
‘data’: {
‘id’: String id,
‘name’: String name,
‘age’: int age,
}
} => ActiveUser(id: id, name: name, age: age),
// パターンB: ゲストユーザーの構造に一致する場合
{
‘status’: ‘guest’,
‘data’: {
‘token’: String token,
}
} => GuestUser(guestToken: token),
// パターンC: エラー情報の構造に一致する場合
{
‘status’: ‘error’,
‘message’: String errorMsg,
} => ApiError(message: errorMsg),
// デフォルト(網羅性チェック:どれにも当てはまらない場合)
_ => ApiError(message: ‘Unknown or malformed response structure’),
};
}
void main() {
// テストケース1: アクティブユーザーのJSON
const json1 = ‘{“status”: “success”, “data”: {“id”: “U123”, “name”: “Alice”, “age”: 28}}’;
// テストケース2: 予期せぬデータ構造
const json2 = ‘{“status”: “success”, “data”: “broken_data”}’;
final result1 = parseUserResponse(json1);
print(‘Result 1 type: ${result1.runtimeType}’); // 出力: ActiveUser
final result2 = parseUserResponse(json2);
print(‘Result 2 type: ${result2.runtimeType}’); // 出力: ApiError
}
コードのここがポイント!
- `switch (decoded)` 式:
Dart 3では、`switch`を「文(Statement)」だけでなく「式(Expression)」として使えます。そのため、パース結果をそのまま`return`の右側に書くことができます。
- 構造化パターンのネスト:
`{‘status’: ‘success’, ‘data’: {‘id’: String id, …}}` の部分に注目してください。JSONのネスト構造をそのままパターンとして記述しつつ、同時に `String id` と宣言することで、「型チェックと変数への代入を1行で同時に行う」ことに成功しています。
- 網羅性(Exhaustiveness):
今回は `_`(ワイルドカード)でフォールバックを用意していますが、対象が `sealed class` の場合などは、Dartのコンパイラが「すべてのパターンを網羅しているか」を静的にチェックしてくれます。
—
3. 陥りやすい文法エラーと注意点
パターンマッチングを使ったJSONパースは非常に強力ですが、初学者のうちは以下のようなトラップにハマりがちです。気をつけていきましょう!
トラップ1: `jsonDecode` の戻り値は `dynamic` (または `Object?`)
`jsonDecode()` は実行時まで何が返ってくるか分からないため、戻り値の型は `dynamic` または `Object?` です。そのため、そのままではプロパティにアクセスできません。
必ず `switch (decoded)` のように、一度パターンマッチングのフィルターに通して、コンパイラに「これはこういう構造のデータである」と証明させる必要があります。
トラップ2: 型キャストのミスマッチ
例えば、JSONの数値はAPIによっては文字列(`”28″`)で送られてきたり、整数(`28`)で送られてきたりします。
もしパターンで `’age’: int age` と書いているときに、JSON側が `{“age”: “28”}`(文字列)だと、このパターンはマッチしません(エラーにならず、スルーされて `_` のフォールバックに落ちます)。
実務では、型が揺らぐ可能性がある場所では、一度 `Object?` として受け取ってから安全に変換する一手間を入れるのが安全です。
// 例:型が曖昧なものを安全にキャッチして変換するパターン
{
‘status’: ‘success’,
‘data’: {
‘age’: Object? rawAge,
}
} => ActiveUser(
…,
age: rawAge is int ? rawAge : int.tryParse(rawAge.toString()) ?? 0,
)
—
まとめ
いかがでしたか?
Dart 3のパターンマッチングを使いこなせるようになると、外部との境界線(APIレスポンスやローカルストレージからの読み込みなど)におけるコードが劇的に美しく、そして堅牢になります。
「データがどういう形をしているべきか」をコードの見た目(パターン)として直感的に表現できるのは、開発していて本当に気持ちがいいものです。ぜひ、あなたのプロジェクトのAPIクライアント層にも取り入れてみてくださいね。
ここをクリアできれば、あなたのDartの基礎力はもうバッチリマスターできています!明日からのコーディングをさらに楽しんでいきましょう。