15章:外部データを信用しない② バリデーション(検証)の設計 ✅🧪
15-0. バリデーションって何?なんでACLでやるの?🛡️✨
外部APIのデータは、ざっくり言うと 「他人の家の荷物」 です📦 中身が壊れてたり、説明と違ったり、突然ラベルが変わったりします😇
そこでACLでは、内側(ドメイン)に入れる前に“検品” します👀✅ これがバリデーション(検証)です!
- ACLのバリデーション:外部のクセ・事故データを入口で止める🚧
* ドメインの不変条件(第8章):アプリのルールを最後まで守り切る🔒
この2段構えがあると最強です💪✨

15-1. 何をチェックする?(チェック項目カタログ)🧾✅
外部データの検証は、だいたい次の4カテゴリに分けると迷いません🧠✨
A) 必須チェック(存在する?)🧱
- 必須フィールドが あるか
nullが来ていいのか(ダメなら弾く🙅♀️)
例:studentId が無い学生データは、もう学生じゃない…😇
B) 型・形式チェック(見た目が正しい?)🔤
- 文字列?数値?配列?オブジェクト?
- 日時形式(ISOっぽい?)⏰
- メール形式、UUID形式、固定桁数など📧
C) 範囲・長さチェック(常識の範囲?)📏
- 数値の範囲:
0以上、上限 - 文字列長:
1〜50文字など - 配列長:
0〜100件など
D) 値の集合チェック(許可された値?)🧩
- enum / 区分値(例:
"1"|"2"だけOK) - ステータス(
"OK"|"NG"|"PENDING"だけOK)
外部APIで一番事故るの、ここです💥 「知らない区分値」が急に増えます😇
15-2. 失敗したらどうする?(3つの方針)🚦
バリデーション失敗時の扱いは、先に方針を決めるのが設計です✍️✨ おすすめは次の3パターンを状況で使い分けること👇

方針①:落とす(エラーで止める)🧯❌
- お金・決済・本人確認など 重要データ は基本これ💰
- “間違った成功”が一番怖い😱
方針②:代替値にする(安全なデフォルト)🧸
- 表示用データなどで、多少の欠損が許せる場合
- 例:
nicknameが欠けてたら"(unknown)"にする
⚠️ただし! 代替値が ドメインの意味を変える なら危険です☠️(例:残高が欠けたから0円扱い、は事故🔥)
方針③:保留・隔離(Quarantine)🧊📦
- 使えないデータを 「隔離してログに残す」
- 後で原因調査できるし、外部仕様変更に強い💪
15-3. 「パース」と「バリデーション」を分けると超ラク🧼➡️✅
第14章の“整形(パース)”と、第15章の“検証(バリデーション)”は、分けると頭が整理しやすいです🧠✨
- パース:型変換・トリム・全角半角・文字→数値 など(整える)🧽
- バリデーション:必須・範囲・形式・許可値(正しいか確かめる)✅
この順番が気持ちいい👇 整える → 確かめる → ドメイン型にする 🎯
15-4. 2026年の定番:スキーマバリデーションで入口を固める🧱✨
TypeScript界隈で「unknownなJSONを型安全にする」用途だと、スキーマバリデーションが超定番です📦✅ 2026年1月時点でも、Zodは利用者が多く、最新版はv4系です(npmの最新表示)(npm) (TypeScript自体も 5.9.x が最新リリースとして案内されています)(GitHub)
ここでは例として Zod を使います🧡 ※軽量志向なら Valibot も選択肢(npm上で最新版が確認できます)(npm)
15-5. 実装の基本形(safeParseで“失敗を値として扱う”)🧪✨
Zodの基本はこれです👇
safeParse:成功/失敗を 例外じゃなく戻り値 で受け取れる(扱いやすい!)🙌
import { z } from "zod";
// 外部から来る「整形後データ」を想定(第14章でパース済み、という気持ち)
const StudentDtoSchema = z.object({
stu_id: z.string().min(1).max(20).regex(/^[A-Z0-9_-]+$/),
stu_kbn: z.string().regex(/^(1|2)$/), // 例:1=学部, 2=院
point: z.number().int().min(0).max(999_999),
updated_at: z.string().datetime({ offset: true }), // ISO + タイムゾーン込み想定
mail: z.string().email().optional(),
});
export type StudentDto = z.infer<typeof StudentDtoSchema>;
export function validateStudentDto(input: unknown) {
const r = StudentDtoSchema.safeParse(input);
if (!r.success) {
return {
ok: false as const,
error: {
kind: "AclValidationError" as const,
source: "StudentApi" as const,
issues: r.error.issues.map((i) => ({
path: i.path.join("."),
code: i.code,
message: i.message,
})),
},
};
}
return { ok: true as const, value: r.data };
}
ポイントはここ👇✨
- 入力は
unknownで受ける(外部は信用しない宣言🛡️) - 失敗をthrowしない(後で「落とす/隔離/代替値」の方針に分岐しやすい🚦)
15-6. “境界で落とす”が効く理由(内側が平和になる)🕊️✨
バリデーションをACLでやらないと、内側がこうなります👇😵💫
- 画面・ユースケース・ドメインのあちこちに
if (!x) return ...が散らばる - どこが入口で、どこが例外なのか不明
- テストも地獄🧪💦
ACLで入口を固めると👇😊
- 内側は「正しい前提」でコードが書ける
- ドメインのルール(不変条件)だけに集中できる
- “外部の事故”はACLだけ見れば追える🔍
15-7. 失敗時の3方針をコードに落とす(例)🚦💡
① 落とす(エラーで止める)🧯
const v = validateStudentDto(raw);
if (!v.ok) {
// ここでログ&エラー返却(第16〜17章で「翻訳されたエラー」にする)
throw new Error(`Invalid external payload: ${JSON.stringify(v.error.issues)}`);
}
② 代替値(安全なデフォルト)🧸
const v = validateStudentDto(raw);
if (!v.ok) {
// mailだけが無くてもOK、みたいな設計なら
// ただし「意味が変わる代替」はNGだよ⚠️
return { ok: true as const, value: { /* ここで安全なデフォルト */ } };
}
③ 隔離(Quarantine)🧊📦
const v = validateStudentDto(raw);
if (!v.ok) {
// 「外部が変わった」証拠を残すのが大事!
console.warn("[ACL] Quarantine payload", { issues: v.error.issues, raw });
return { ok: false as const, error: v.error };
}
15-8. ありがちな“検証の落とし穴”5選 😇🪤
- 「必須だけ」チェックして安心する(範囲/許可値が抜けがち)
- 代替値でごまかしすぎる(サイレント事故の元🔥)
- ログが無い(外部変更に気づけない🚨)
- 検証ルールが散る(ACLの中に“ひとまとめ”が基本🧱)
- 失敗を例外にしすぎる(制御が読みにくい😵)
15-9. ミニ演習:検証ルールを追加してみよう🎓✨
次のルールを StudentDtoSchema に追加してみてね👇💪
stu_idはAから始まる(例:A00123)🪪pointは 0〜100000 の範囲に縮める(上限を現実に合わせる)📏updated_atは「未来日」だったら隔離(時計ズレ対策)⏳🧊
ヒント:
- 形式チェックは
regex、複合条件はrefineが便利だよ✨
15-10. AI活用コーナー:バリデーションは“下書き生成”が相性最高🤖🧡
バリデーションは「型・範囲・形式」の列挙が多いので、AIが得意です✍️✨ ただし 最終判断は人間が監督 ね🛡️
使える指示例(コピペOK)📋
- 「このJSONサンプルからZodスキーマを作って。必須/optionalも推測して」
- 「このAPIの“事故りそうポイント”を挙げて、検証ルール案を10個出して」
- 「safeParse失敗時のエラーを、path付きで整形して返す関数を書いて」
⚠️注意:外部レスポンスに個人情報が含まれる場合は、マスキングしてから貼ろうね🔒
15-11. まとめ:第15章で持ち帰ること🎒✨
- 外部データは unknown として扱う🛡️
- ACLで 入口検証 をして、内側を平和にする🕊️
- 失敗時の方針は3つ:落とす / 代替 / 隔離 🚦
- スキーマバリデーション(例:Zod)で ルールを1箇所に集約 🧱✅
次章(第16章)では、ここで出た「失敗」を 外部エラーとドメインエラーに分ける ところに進むよ🧊🔥