開発チームの皆さん、お疲れ様です。テクニカルリードの私だ。
今日のコードレビューで、また「お祈り型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
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
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
),
// パターンB: 一括注文のレスポンス {“status”: “bulk_success”, “orders”: [ … ]}
{
‘status’: ‘bulk_success’,
‘orders’: List rawOrders,
} =>
BulkOrderSuccess(
orders: rawOrders
.whereType
// パターン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
return switch (map) {
{
‘order_id’: String orderId,
‘total’: num total,
‘items’: List items,
} =>
OrderSuccess(
orderId: orderId,
totalAmount: total.toInt(),
items: items.whereType
),
_ => 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
—
パフォーマンスと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層へ伝えること。
明日からの君たちのプルリクエストで、野良キャストが綺麗に駆逐されていることを期待している。
質問があればいつでも私のデスクに来たまえ。プロフェッショナルなコードを書いていこう。