Dart 3パターンマッチングで制圧する:型安全なJSONバリデーションとドメインモデリングの極意
コードレビューをしていて、未だに以下のようなコードを見かけると、私は技術的負債の匂いを察知して眉をひそめる。
// ⚠️ 典型的な「やりがち」なアンチパターン
final json = jsonDecode(response.body) as Map
if (json.containsKey(‘user’) && json[‘user’] is Map) {
final userJson = json[‘user’] as Map
final name = userJson[‘name’] as String?;
final age = userJson[‘age’] as int?;
if (name != null && age != null) {
return User(name: name, age: age);
}
}
throw FormatException(‘Invalid JSON’);
冗長な型キャスト、散らばる `null` チェック、そして実行時エラー(`TypeError` や `FormatException`)への無防備な姿勢。外部APIのレスポンスは、いわば「信頼できない野良データ」である。これを場当たり的な `as` キャストで処理するのは、地雷原を目隠しで歩くようなものだ。
Dart 3で導入されたパターンマッチング(Pattern Matching)とレコード(Records)は、この構造的な脆弱性をコンパイル時に根絶するための強力な武器となる。
本記事では、DartのコンパイラとVMの挙動を意識しつつ、外部APIのレスポンスを美しく、かつ極限まで堅牢に型安全なドメインモデルへ変換する実践的アーキテクチャを伝授する。
—
1. なぜ従来のJSONパースは「脆弱」なのか?
`jsonDecode` が返すのは `Object?` であり、実体はただの `Map
闇雲なキャスト(`as String` など)は、型が一致しなかった瞬間にプロダクション環境でアプリをクラッシュさせる。かといって、すべてのフィールドに細かく `is` チェックを書くのは、コードの認知負荷を跳ね上げ、メンテナンス性を殺す。
Dart 3のパターンマッチングは、この「データの形状(Shape)の検証」と「変数への束縛(Binding)」を単一の式(Expression)として同時に安全に行う。
—
2. 実践:厳密なJSONデシリアライゼーションの設計
ここでは、次のような複雑なAPIレスポンスを想定する。
- 成功レスポンス: ステータスコードと、ユーザーデータ、またはシステムエラー情報が含まれる。
- ユーザーデータの `role` は `admin` または `guest` のみ許容される。
プロダクションコード例
以下のコードは、パターンマッチングを駆使して外部データを一網打尽に検証・変換する、妥協のない実装である。
import ‘dart:convert’;
// — ドメインモデル —
sealed class DomainResult {}
class Success extends DomainResult {
final User user;
Success(this.user);
}
class ApiError extends DomainResult {
final String message;
ApiError(this.message);
}
class User {
final String id;
final String name;
final UserRole role;
User({required this.id, required this.name, required this.role});
}
enum UserRole { admin, guest }
// — バリデーション & パーサー —
DomainResult parseApiResponse(String rawJson) {
// 1. まず全体がJSONのMapであるかを安全にキャスト
final Object? decoded;
try {
decoded = jsonDecode(rawJson);
} catch (_) {
return ApiError(‘Malformed JSON syntax.’);
}
// 2. Dart 3の構造化パターンマッチングによる検証
// ここでデータの「型」と「形状(キーの存在)」を同時に精査する
return switch (decoded) {
// パターン①: 期待する成功レスポンスの構造
{
‘status’: ‘success’,
‘data’: {
‘id’: String id,
‘name’: String name,
‘role’: String roleStr,
}
} => _mapToUserSuccess(id, name, roleStr),
// パターン②: API側が返すエラーメッセージ構造
{
‘status’: ‘error’,
‘error’: {‘message’: String errorMessage}
} => ApiError(errorMessage),
// パターン③: 想定外のスキーマ(フォールバック)
_ => ApiError(‘Unexpected JSON schema received.’),
};
}
DomainResult _mapToUserSuccess(String id, String name, String roleStr) {
// 値のドメイン制約バリデーション(Enumへの安全なマッピング)
final role = switch (roleStr) {
‘admin’ => UserRole.admin,
‘guest’ => UserRole.guest,
_ => null, // 不明なロールは弾く
};
if (role == null) {
return ApiError(‘Invalid user role: $roleStr’);
}
return Success(User(id: id, name: name, role: role));
}
// — 動作確認用main関数 —
void main() {
// テストケース1: 正常系
const json1 = ”’
{
“status”: “success”,
“data”: {
“id”: “u_9981”,
“name”: “Alice”,
“role”: “admin”
}
}
”’;
// テストケース2: 異常系(型違い・構造違い)
const json2 = ”’
{
“status”: “success”,
“data”: {
“id”: 12345,
“name”: “Bob”,
“role”: “superuser”
}
}
”’;
print(‘Result 1: ${parseApiResponse(json1).runtimeType}’); // Success
print(‘Result 2: ${parseApiResponse(json2).runtimeType}’); // ApiError
}
—
3. この設計が優れている理由(アーキテクチャの視点)
① 網羅性チェック(Exhaustiveness Checking)の恩恵
Dartの `switch` 式は、入力されうる全てのパターンが網羅されているかをコンパイル時に検証する。将来APIの仕様が変わり、新しいステータスが追加された際、ハンドリングし忘れるとコンパイルエラーになるため、デプロイ前にバグを封じ込めることができる。
② 実行時コストの最小化
Dart VMの観点から見ても、このパターンマッチングは非常に効率的に最適化される。複数の `if-else` や `containsKey` の連打は分岐予測のミスを誘発しやすいが、構造化パターンはJVMのネイティブコード生成やDart AOTコンパイラにおいて、効率的なジャンプテーブルやインライン化された型チェックに変換されやすい。
③ 責務の完全な分離
「JSONがどういう構造であるべきか」というスキーマの定義と、「それをどうドメインモデルに落とし込むか」が、1つの `switch` 式の中に宣言的に記述されている。これにより、コードの可読性が劇的に向上し、レビュー時に「API仕様の変更箇所」が一目でわかるようになる。
—
4. パフォーマンス上の注意点と実務の鉄則
どれほど強力な機能であっても、使い所を誤ればパフォーマンスを害する。以下の2点は実務において必ず心に留めておいてほしい。
1. 巨大なJSONツリーの全域マッチを避ける
数千要素の配列を持つ巨大なJSON全体を1つのパターンでマッチさせようとすると、コンパイル時間の増大やVMのメモリ効率低下を招く。配列や深い階層を持つデータは、チャンクに分割してパーースする(ドメインごとにパーサ関数を切り出す)のが定石である。
2. 生成コード(JSON Serializable等)との使い分け
ボイラープレートを減らすために `json_serializable` などのコード生成ツールを使うアプローチもある。しかし、外部APIが頻繁に仕様変更される、あるいはレガシーで歪んだJSON構造を吸収しなければならないドメイン境界においては、今回紹介した手動のパターンマッチングによるアダプター層の構築の方が圧倒的に柔軟で安全である。
—
チーフアーキテクトからの提言
型安全とは、単に「エラーが出ないこと」ではない。「不正なデータがドメイン層へ侵入する余地をコンパイラによって完全に断つこと」である。
Dart 3のパターンマッチングは、単なるシンタックスシュガーではない。それは、フロントエンドやAPIクライアントのコードベースを「信頼の置けない外部世界」から守るための堅牢な城壁なのだ。
今日からあなたのプロジェクトのAPIレイヤーを見直し、場当たり的なキャストをこの美しいパターンマッチングに置き換えてほしい。コードの匂いが変わり、チーム全体の開発生産性が一段階引き上げられることを、私が保証しよう。