【実務・中級編】Dart 3のパターンマッチングでJSONパーサーを自作する – Dart コア文法・オブジェクト指向・Null安全解析バイブル

開発チームの皆さん、お疲れ様です。テクニカルリードの私だ。

今日のコードレビューで、また「お祈り型JSONパース(`as Map` の山)」を見かけたので、いい加減にチーム全体の共通認識としてこの問題を根絶するため、記事を書くことにした。

外部APIから飛んでくるJSONを `jsonDecode` し、野良のキャストや、サードパーティのコードジェネレーター(〇〇_serializableなど)に思考停止で頼り切る開発スタイルは今日で終わりにする。
Dart 3で導入されたパターンマッチング(Pattern Matching)を正しく使えば、外部からの入力という「型不明な爆弾」を、コンパイル時に完全に安全なドメインモデルへと、美しく、かつ圧倒的なパフォーマンスで変換できる。

今回は、実務の現場で即座にコピー&ペーストして使えるプロダクションクオリティの「ゼロ依存JSONパーサー」の設計と、Dart VMの裏側の動きまで踏み込んだ極限の知見を伝授する。

—

なぜ「お祈りキャスト」と「過剰なコード生成」は悪なのか

実務でよく見るアンチパターンを挙げてみこう。

// 良くあるアンチパターン:型安全ではない、保守性の低いコード
final data = jsonDecode(response.body) as Map;
final user = User(
id: data[‘id’] as int, // ここでTypeErrorが起きるまで気づけない
name: data[‘name’] as String,
email: data[‘email’] as String?,
);

このコードの問題点は、「実行時までスキーマの不一致に気づけない」こと、そして「JSONの構造変化(キーの欠損や型の変化)に対して極めて脆弱」なことだ。かといって、すべてのAPIレスポンスのために重厚長大なコードジェネレーターを導入すると、ビルド時間の肥大化や、生成コードのブラックボックス化に悩まされることになる。

Dart 3のパターンマッチングは、このジレンマを鮮やかに解決する。ランタイムコストを極限まで削ぎ落とし、網羅性チェック(Exhaustiveness Checking)をコンパイラに強制させながら、人間にとって最も読みやすい宣言的コードを書くことができるのだ。

—

プロダクション実装:堅牢なJSONパーサーの全体像

今回は、ECサイトの注文履歴APIを想定しよう。APIレスポンスには、「成功(単体注文)」「成功(複数一括注文)」「エラー」の3パターンが混ざって返ってくるカオスな仕様とする。

これらをDart 3の `switch` 式とパターンマッチングを駆使して、一刀両断にパースするコードが以下だ。

import ‘dart:convert’;

// — 1. ドメインモデルの定義 (Sealed Classで状態を完全網羅) —
sealed class ApiResponse {}

class OrderSuccess extends ApiResponse {
final String orderId;
final int totalAmount;
final List items;

OrderSuccess({
required this.orderId,
required this.totalAmount,
required this.items,
});

@override
String toString() => ‘OrderSuccess(id: $orderId, amount: $totalAmount, items: $items)’;
}

class BulkOrderSuccess extends ApiResponse {
final List orders;

BulkOrderSuccess({required this.orders});

@override
String toString() => ‘BulkOrderSuccess(count: ${orders.length})’;
}

class ApiError extends ApiResponse {
final int errorCode;
final String message;

ApiError({required this.errorCode, required this.message});

@override
String toString() => ‘ApiError($errorCode: $message)’;
}

// — 2. 堅牢なパーサー本体 —
class OrderApiResponseParser {

/// JSONの文字列を受け取り、網羅的かつ安全にドメインモデルへ変換する
static ApiResponse parse(String rawJson) {
// 1. まずJSONとしてのデコード安全性を担保
final dynamic decoded;
try {
decoded = jsonDecode(rawJson);
} catch (e) {
return ApiError(errorCode: -1, message: ‘Invalid JSON format: $e’);
}

// 2. Dart 3のスイッチ式 + オブジェクトパターンによるマッチング
return switch (decoded) {
// パターンA: 単体注文のレスポンス {“status”: “success”, “data”: { … }}
{
‘status’: ‘success’,
‘data’: {
‘order_id’: String orderId,
‘total’: num total, // intかdoubleか揺れがある場合はnumで受けて安全にキャスト
‘items’: List rawItems,
}
} =>
OrderSuccess(
orderId: orderId,
totalAmount: total.toInt(),
// リストの要素も安全にパースする
items: rawItems.whereType().toList(),
),

// パターンB: 一括注文のレスポンス {“status”: “bulk_success”, “orders”: [ … ]}
{
‘status’: ‘bulk_success’,
‘orders’: List rawOrders,
} =>
BulkOrderSuccess(
orders: rawOrders
.whereType>()
.map((o) => _parseSingleOrderMap(o))
.whereType()
.toList(),
),

// パターンC: エラーレスポンス {“error_code”: int, “message”: String}
{
‘error_code’: int code,
‘message’: String msg,
} =>
ApiError(errorCode: code, message: msg),

// フォールバック(スキーマ違反)
_ => ApiError(errorCode: -99, message: ‘Unrecognized response schema: $decoded’),
};
}

/// 内部ヘルパー:個別注文マップのパース
static OrderSuccess? _parseSingleOrderMap(Map map) {
return switch (map) {
{
‘order_id’: String orderId,
‘total’: num total,
‘items’: List items,
} =>
OrderSuccess(
orderId: orderId,
totalAmount: total.toInt(),
items: items.whereType().toList(),
),
_ => null, // 不正な要素はスキップ用
};
}
}

// — 3. 実行検証 —
void main() {
// テストケース1: 正常系(単体注文)
const json1 = ”’
{
“status”: “success”,
“data”: {
“order_id”: “ORD-2023-999”,
“total”: 5400,
“items”: [“Dart 3 Guide”, “Flutter in Action”]
}
}
”’;

// テストケース2: 異常系(スキーマ破壊)
const json2 = ‘{“status”: “success”, “data”: {“order_id”: 12345}}’;

print(OrderApiResponseParser.parse(json1));
// 意図した通りに出力される: OrderSuccess(id: ORD-2023-999, amount: 5400, items: [Dart 3 Guide, Flutter in Action])

print(OrderApiResponseParser.parse(json2));
// 安全にエラーにフォールバック: ApiError(errorCode: -99, message: …)
}

—

アーキテクトが解説する:この設計が「圧倒的に優れている」理由

上記のコードには、単なる構文の置き換えにとどまらない、Dartのランタイム特性を考慮した深い設計上の工夫が詰まっている。

1. `switch` 式による「構造の視覚化」

従来の `if-else` によるキーの存在チェックとキャストの嵐に比べ、Dart 3のスイッチパターンは「期待するJSONのツリー構造をそのままコードの形に落とし込んでいる」。
コードレビューの際、レビュアーは「このAPIがどのようなJSONを期待しているのか」をパターンの形状を見るだけで直感的に把握できる。

2. `num` 型の活用と安全な数値変換

APIサーバーの実装言語(Python, Go, Node.jsなど)によっては、整数を返すはずのフィールドが仕様変更や浮動小数点の絡みで `double` や文字列として返ってくることが稀によくある。
ここで `int` で厳密に受けようとすると一発でクラッシュするが、パターンマッチで `num total` として受けておき、最後に `.toInt()` を挟むことで、「構造の型安全性を担保しつつ、値の微細な揺らぎには柔軟に対応する」という、実務で極めて有効なディフェンシブ・プログラミングが成立する。

3. `whereType()` によるリストの汚染防止

`List rawItems` の中身が必ず `String` であるとは限らない(外部APIを信用するな)。
`rawItems.whereType()` を使うことで、リスト内に混入した `null` や予期せぬ数値オブジェクトをコンパイル&実行時安全にフィルターし、型安全な `List` をノーコストで生成できる。

—

パフォーマンスとDart VMの裏側の話

「こんな複雑なパターンマッチングを毎回実行したら、UIスレッドが重くなるのでは?」と心配するジュニアエンジニアがいるかもしれない。安心してほしい。

DartのAOT(Ahead-Of-Time)コンパイラおよびJITコンパイラは、Dart 3のパターンマッチングを非常に効率的なジャンプテーブルや型チェックの最適化木(Decision Tree)にコンパイルする。
手動で書いた複雑なネストした `if-else` と比較してパフォーマンス上のペナルティは一切なく、むしろコンパイラが最適化を行いやすい構造になっている。

ただし、巨大なJSONツリー(数万要素の配列など)全体を一度にパターンマッチングにかけようとすることは避けるべきだ。
重いペイロードを扱う場合は、ストリーミングパーサー(`json.fuse(utf8.decoder).bind(…)` など)を併用し、パースの単位を適切にチャンク(分割)すること。これはI/Oバウンドな非同期処理における基本原則である。

—

本日のまとめとレビュアーからの申し送り事項

1. 外部入力を「信じるな」。 `as` キャストでの直接代入はコードレビューで即リジェクトする。
2. Dart 3のパターンマッチングを使え。 JSONの構造をそのままコードの形に表現し、可読性と安全性を最大化せよ。
3. 失敗を隠すな、ドメインモデルに閉じ込めよ。 パース失敗時はクラッシュさせるのではなく、`ApiError` などのSealed Classに安全に落とし込み、UI層へ伝えること。

明日からの君たちのプルリクエストで、野良キャストが綺麗に駆逐されていることを期待している。
質問があればいつでも私のデスクに来たまえ。プロフェッショナルなコードを書いていこう。

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