【実務・中級編】Dartの「パターンマッチング」でJSONのバリデーションを型安全に行う手法 – Dart コア文法・オブジェクト指向・Null安全解析バイブル

コードレビューをしていて、最も目を覆いたくなる瞬間の一つがこれだ。

// どこにでもある「動くが脆い」コード
final json = jsonDecode(response.body) as Map;
final username = json[‘user’][‘name’] as String; // 💥 NullPointerException or TypeError

外部APIから飛んでくるJSONを、信じきってキャストする。スキーマが変わった瞬間、あるいは予期せぬ`null`が混入した瞬間にプロダクション環境でアプリがクラッシュする。TypeScriptであればZodやValibotを使う領域だが、モダンなDart(Dart 3以降)には、コンパイラと完全に統合された強力な「パターンマッチング」という最強の武器がある。

今回は、外部から受け取った未知のJSONを、Dart 3のパターンマッチングを用いてコンパイル時の安全性と極限のパフォーマンスを両立させながらバリデーションし、ドメインモデルへ昇華させる手法を解説しよう。

—

なぜ従来の `fromJson` は破綻するのか?

多くの開発者は、次のようなファクトリーコンストラクタを書く。

class User {
final String id;
final String name;

User.fromJson(Map json)
: id = json[‘id’] as String,
name = json[‘name’] as String;
}

このコードの何が問題か?
1. 実行時エラーの隠蔽: キャスト(`as String`)は、型が一致しなかった場合に容赦なく`TypeError`をスローする。
2. 構造の検証不足: `id`や`name`が欠損している場合や、想定外の型(例えば数値やネストされたオブジェクト)が来た場合のフォールバックがない。
3. 網羅性の欠如: エラーハンドリングが場当たり的になり、コードベース全体に不確実性が伝播する。

Dart 3のパターンマッチングと代数データ型(ADT)の概念を導入すれば、「不正なJSONをそもそもドメイン層に入れない」堅牢な境界(Boundary)を構築できる。

—

実装:パターンマッチングによる型安全JSONバリデーション

以下のプロダクションコードを見てほしい。外部APIからのレスポンスを安全に検証し、成功・失敗を型安全にハンドリングする完全な例だ。

import ‘dart:convert’;

// — 1. ドメインモデルの定義 —
sealed class ApiResult {
const ApiResult();
}

class ApiSuccess extends ApiResult {
final T data;
const ApiSuccess(this.data);
}

class ApiError extends ApiResult {
final String message;
const ApiError(this.message);
}

class UserProfile {
final String id;
final String name;
final int age;

const UserProfile({
required this.id,
required this.name,
required this.age,
});

// — 2. パターンマッチングを活用したバリデーションコンストラクタ —
static ApiResult fromJson(Object? json) {
// Dart 3の構造パターンとガード節による検証
return switch (json) {
// 型チェックとマップ構造の分解を同時に行う
{‘id’: String id, ‘name’: String name, ‘age’: int age} =>
ApiSuccess(UserProfile(id: id, name: name, age: age)),

// 型の不一致やフィールドの欠損がある場合
Map() =>
const ApiError(‘JSONのスキーマ構造が不正です(型またはキーのミスマッチ)’),

// Mapですらない場合(nullやプリミティブ値など)
_ =>
const ApiError(‘ルート要素がオブジェクトではありません’),
};
}
}

// — 3. 実行検証用コード —
void main() {
// パターンA: 正常なJSON
const validJsonString = ‘{“id”: “usr_001”, “name”: “Alice”, “age”: 30}’;

// パターンB: 型が不正なJSON(ageが文字列になっている)
const invalidTypeJsonString = ‘{“id”: “usr_002”, “name”: “Bob”, “age”: “30”}’;

// パターンC: キーが欠損しているJSON
const missingKeyJsonString = ‘{“id”: “usr_003”, “name”: “Charlie”}’;

for (final raw in [validJsonString, invalidTypeJsonString, missingKeyJsonString]) {
print(‘— 検証開始 —‘);
final decoded = jsonDecode(raw);

// パターンマッチング(switch式)による結果のハンドリング
// sealed classのおかげでコンパイラがすべてのケース(Success/Error)を網羅しているか強制する
final result = UserProfile.fromJson(decoded);

switch (result) {
case ApiSuccess(data: final user):
print(‘✅ 成功: ${user.name} (ID: ${user.id}, Age: ${user.age})’);
case ApiError(message: final msg):
print(‘❌ 失敗: $msg’);
}
}
}

—

コードの深層解説:なぜこの設計が優れているのか?

1. `switch` 式とオブジェクトパターンの統合

`switch (json)` の部分で、Dart 3のオブジェクトパターン(Object Pattern)とマップパターン(Map Pattern)が同時に機能している。

{‘id’: String id, ‘name’: String name, ‘age’: int age} => …

この一行は、Dart VMの内部で次のような検証を爆速で行っている。

  • `json` が `Map` であることの確認。
  • `’id’`, `’name’`, `’age’` というキーが存在することの確認。
  • それぞれの値が、指定された型(`String`, `String`, `int`)に厳密に一致することの確認。

従来の `if` 文のネストや、手動での `is` キャストの嵐をエレガントに駆逐している。

2. `sealed` クラスによる「網羅性の強制(Exhaustiveness)」

結果を受け取る側(`main` 関数の後半)を見てほしい。`switch (result)` において、`ApiSuccess` と `ApiError` のハンドリングが網羅されているかをDartのコンパイラが静的解析している。もし将来的に `ApiLoading` などの新しい状態を追加した場合、このスイッチ文を書き換えない限りコンパイルエラーになる。バグの温床となる「予期せぬ状態の取りこぼし」を型システムが完全に根絶する。

—

パフォーマンス上の注意点とチーフアーキテクトからの助言

「パターンマッチングは便利だが、実行時コストが高いのではないか?」という懸念を持つ優秀なエンジニアもいるだろう。

Dart VMのAOT/JITコンパイラにおいて、Dart 3のパターンマッチングは高度に最適化されている。内部的には効率的な型ジャンプテーブルや構造検証コードにコンパイルされるため、手動で書く `as` キャストや冗長な `is` チェックと比較してパフォーマンスのペナルティは実質的にゼロである。

ただし、以下の実務上のアンチパターンには注意してほしい。

1. 巨大なJSONの全域パースを避ける
数十MBに及ぶ巨大なJSONツリー全体を一度にパターンマッチングしようとすると、メモリ効率が悪化する。API境界で必要なドメインモデルの単位にJSONを分割し、レイヤーごとにバリデーション境界を作るべきだ。
2. 動的なキー(Dynamic Keys)への過信
キー名が動的に変わる辞書データ(例: `Map>`)を検証する場合は、マップパターンだけでなく、イテレータブルな走査やカスタムガード節を組み合わせる必要がある。

—

まとめ

外部データとの境界(Boundary)は、アプリケーションの信頼性を決める最重要防壁だ。
「動けばいいや」のキャストコードは、チームの成長とともに技術的負債へと変わり、深夜の障害対応というコストを会社に支払わせることになる。

Dart 3のパターンマッチングをマスターし、「不正なデータはコンパイルおよび境界の時点で一切通さない」という強固なマインドセットを君のプロダクションコードに導入してほしい。コードは美しくなり、バグは消滅し、コードレビューはもっと知的で生産的な議論へと進化するはずだ。

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