「入力値はちゃんとチェックしてるし、大丈夫でしょ」
そう思っていたのに、思わぬところでバグが出てしまった経験、ありませんか?実は、「バリデーション(検証)」と「パース(解析・変換)」は似ているようで、全然違うアプローチなんですよね。
最近、海外の技術コミュニティで 「Parse, Don’t Validate」 というコンセプトが改めて注目を集めています。今回はこのアイデアをTypeScript・Python両方の視点でかみ砕いて解説していきます 🚀
「バリデーション」と「パース」何が違うの?

ざっくり一言で言うと、こんな違いです。
- ✅ バリデーション:「この値は正しい形式か?」を確認するだけ。結果は true / false。
- 🔄 パース:「この値を受け取って、安全な型・構造に変換する」。結果は変換済みのデータ。
イメージとしては、バリデーションは「ドアの前で身分証を確認するだけ」、パースは「確認しつつ専用の入館バッジを渡す」感じです。バリデーションだけだと「チェックしたはずなのに後で使うときに型が保証されていない」という問題が起きやすいんです。
TypeScriptで見る「バリデーションだけ」の問題点
まず「バリデーションだけ」の典型的なコードを見てみましょう。
// ❌ バリデーションだけのパターン
function isValidUser(data: unknown): boolean {
return (
typeof data === "object" &&
data !== null &&
typeof (data as any).name === "string" &&
typeof (data as any).age === "number"
);
}
// 使う側のコード
const rawData: unknown = JSON.parse(someJson);
if (isValidUser(rawData)) {
// TypeScriptはここでも rawData の型は unknown のまま!
// (data as any).name のようにキャストが必要になる
console.log((rawData as any).name); // 型安全じゃない😰
}
このコードの何が問題かというと、isValidUser が true を返しても、TypeScriptの型システムは「rawData がユーザー型だ」とは認識してくれないんです。結果として as any キャストが必要になり、型安全性がどんどん崩れていきます。
「型ガード」で少し改善してみる
TypeScriptには「型ガード」という機能があって、value is SomeType という書き方をすると少し改善できます。
type User = {
name: string;
age: number;
};
// 型ガードを使ったバリデーション
function isUser(data: unknown): data is User {
return (
typeof data === "object" &&
data !== null &&
typeof (data as any).name === "string" &&
typeof (data as any).age === "number"
);
}
const rawData: unknown = JSON.parse(someJson);
if (isUser(rawData)) {
// ここでは rawData の型が User として扱われる ✅
console.log(rawData.name); // 型安全!
}
型ガードはかなり改善されていますが、それでも「バリデーションをパスした後も、アプリのあちこちで再チェックが必要になる」という問題は残ります。入口で一度チェックして「安全な型のオブジェクト」として返してしまう「パース」の方が、より堅牢な設計になるんです。
「パース」アプローチに切り替えると何が変わるか
「パース」の考え方では、入力データを受け取ったらその場で安全な型のオブジェクトに変換して返すようにします。変換できなければエラーを投げる(またはnullを返す)。
type User = {
name: string;
age: number;
};
// ✅ パースのパターン:変換して返す
function parseUser(data: unknown): User {
if (
typeof data !== "object" ||
data === null ||
typeof (data as any).name !== "string" ||
typeof (data as any).age !== "number"
) {
// 不正なデータは「入口」で弾く
throw new Error(`Invalid user data: ${JSON.stringify(data)}`);
}
// 検証済みの安全なオブジェクトを返す
return {
name: (data as any).name,
age: (data as any).age,
};
}
// 使う側のコード
try {
const user = parseUser(JSON.parse(someJson));
// user は確実に User 型 🎉
console.log(user.name); // キャスト不要!
console.log(user.age);
} catch (e) {
console.error("不正なデータです:", e);
}
こうすると、parseUser を通過したデータはアプリ内のどこで使っても確実に User 型です。「チェックしたはずなのに型が怪しい…」という不安が消えます。
Result型パターンでエラーハンドリングも整理する
try-catch が増えると読みにくくなることもあるので、Result型(成功/失敗を型で表現する)と組み合わせるとさらにスッキリします。
type Result<T> =
| { ok: true; value: T }
| { ok: false; error: string };
function parseUser(data: unknown): Result<User> {
if (
typeof data !== "object" ||
data === null ||
typeof (data as any).name !== "string" ||
typeof (data as any).age !== "number"
) {
return { ok: false, error: "Invalid user data" };
}
return {
ok: true,
value: {
name: (data as any).name,
age: (data as any).age,
},
};
}
// 使う側
const result = parseUser(JSON.parse(someJson));
if (result.ok) {
console.log(result.value.name); // ✅ 型安全
} else {
console.error(result.error); // ✅ エラー内容も型安全
}
例外を使わず、成功・失敗を型で明示できるのでとても読みやすくなりますよね。
ZodでParse, Don’t Validateを実践する(TypeScript)
「毎回自分でパース関数を書くのは大変…」そう感じた方、ご安心ください。TypeScriptには Zod というライブラリがあって、まさに「パース」の考え方を実践するために作られています。
import { z } from "zod";
// スキーマ定義
const UserSchema = z.object({
name: z.string(),
age: z.number().int().positive(), // 整数かつ正の数という制約も追加できる
email: z.string().email().optional(), // メールアドレス形式のバリデーションも簡単
});
// スキーマから型を自動生成できる!
type User = z.infer<typeof UserSchema>;
// パース(変換)する
const rawData: unknown = JSON.parse(someJson);
// parse() → 失敗すると例外
const user = UserSchema.parse(rawData); // User 型が保証される
// safeParse() → 失敗しても例外なし(Result型っぽい使い方)
const result = UserSchema.safeParse(rawData);
if (result.success) {
console.log(result.data.name); // ✅ 型安全
} else {
console.error(result.error.issues); // ✅ エラー詳細も取得できる
}
Zodを使うと、スキーマ定義・型生成・パースをまとめて1か所で管理できます。「型とバリデーションロジックが二重管理になる」という悩みもなくなるので、現場でも非常によく使われているライブラリです。
Pythonでも「Parse, Don’t Validate」を実践する
このコンセプトはPythonでも同じように使えます。Pythonには Pydantic というライブラリがあって、Zodと同様に「パース」の考え方を実装できます。
バリデーションだけのPythonコード(問題あり)
# ❌ バリデーションだけのパターン(Python)
def is_valid_user(data: dict) -> bool:
return (
isinstance(data.get("name"), str) and
isinstance(data.get("age"), int)
)
raw_data = {"name": "田中太郎", "age": 25}
if is_valid_user(raw_data):
# チェックをパスしても raw_data は dict のまま
# age が本当に int かどうかは「使う側が信じるしかない」
print(raw_data["name"]) # 型アノテーションなし
print(raw_data["age"] + 1) # 実行時エラーのリスクが消えない
Pydanticでパースする(推奨パターン)
from pydantic import BaseModel, EmailStr, ValidationError
from typing import Optional
# ✅ Pydanticでスキーマ定義
class User(BaseModel):
name: str
age: int
email: Optional[str] = None
# パース(変換)する
raw_data = {"name": "田中太郎", "age": "25"} # age が文字列でも...
try:
user = User(**raw_data)
# Pydanticが自動で型変換してくれる!
print(user.name) # str: 田中太郎
print(user.age) # int: 25 (文字列"25"→整数25に変換済み)
print(type(user.age)) # <class 'int'>
except ValidationError as e:
print(f"バリデーションエラー: {e}")
# safeParse的な使い方をしたい場合
try:
user = User.model_validate(raw_data)
except ValidationError as e:
# エラー詳細を構造化データとして取得できる
for error in e.errors():
print(f"フィールド: {error['loc']}, エラー: {error['msg']}")
Pydanticのすごいところは、「25」という文字列を受け取っても、age: int と定義してあれば自動で整数に変換してくれる点です。「ユーザーの入力やAPIレスポンスは型が曖昧なことが多い」という現実に対して、とても実用的な解決策になっています。
Pydanticでより厳格なバリデーションをかける
from pydantic import BaseModel, field_validator, Field
from typing import Optional
class User(BaseModel):
name: str = Field(min_length=1, max_length=50)
age: int = Field(ge=0, le=150) # 0以上150以下
email: Optional[str] = None
@field_validator("name")
@classmethod
def name_must_not_be_empty(cls, v: str) -> str:
if v.strip() == "":
raise ValueError("名前は空白のみにできません")
return v.strip() # 前後の空白を除去して返す
# テスト
try:
user = User(name=" ", age=25)
except Exception as e:
print(e) # バリデーションエラーが出る
# 正常ケース
user = User(name=" 田中太郎 ", age=25)
print(user.name) # "田中太郎"(空白が除去されて返ってくる)
このように、Pydanticの @field_validator を使えば「チェックしながら変換する」ことも同時にできます。「名前の前後の空白を除去して返す」というのはまさにパースの考え方ですね。
「パース」アプローチの3つのメリットまとめ
ここまで見てきたことを整理すると、「バリデーションよりパース」が強い理由は主に3つです。
| 観点 | バリデーションだけ | パース(推奨) |
|---|---|---|
| 型の保証 | チェック後も型が曖昧 | 変換後は確実に正しい型 |
| コードの複雑さ | 使う箇所ごとに再チェックが必要 | 入口で一度パースすれば安心 |
| エラーの発生タイミング | 実行時の深い場所でエラーが起きやすい | 入力時点で即座にエラーを検出 |
- 型安全性が高まる:パースを通過したデータは「確実にこの型」と保証されるので、あちこちにキャストや型チェックを書かなくてよくなります。
- バグを早期発見できる:「入口で弾く」設計なので、不正データがアプリの深い部分まで流れ込みにくくなります。デバッグが断然楽になります。
- コードが読みやすくなる:「このデータはパース済み=安全」という前提でコードが書けるので、防衛的なチェックが減り、本質的なロジックに集中できます。
どこでパースすべき?「信頼境界線」を意識しよう
「パース」アプローチを使うとき、重要なのが「信頼境界線(Trust Boundary)」という考え方です。
信頼境界線とは、「安全が保証されていないゾーン」と「安全が保証されているゾーン」の境目のことです。典型的な場所はこんなところです。
- 🌐 APIのリクエスト受信時(外部からのJSONデータ)
- 📂 ファイル読み込み時(CSVやJSONファイル)
- 🗃️ DBからデータを取り出したとき(スキーマが変わっている可能性)
- ⌨️ ユーザー入力を受け取ったとき(フォームデータなど)
これらの「境界線」でパース関数を通すことで、アプリの内部には常に「安全な型のデータ」しか入らない状態を作れます。
# FastAPI + Pydantic の例:APIエンドポイントでのパース
from fastapi import FastAPI
from pydantic import BaseModel, Field
app = FastAPI()
class CreateUserRequest(BaseModel):
name: str = Field(min_length=1, max_length=50)
age: int = Field(ge=0, le=150)
@app.post("/users")
def create_user(body: CreateUserRequest):
# ここに来た時点で body は確実に CreateUserRequest 型
# バリデーションはFastAPIとPydanticが入口でやってくれている
print(body.name)
print(body.age)
return {"message": f"{body.name}さんを登録しました"}
FastAPI は内部でPydanticを使って自動的にパースしてくれるので、「Parse, Don’t Validate」の考え方が標準で組み込まれています。これもFastAPIが人気な理由のひとつですね。
まとめ:「入口でパース」の習慣を身につけよう
今回解説した「Parse, Don’t Validate」のポイントをまとめます。
- ✅ バリデーションは「true/false を返すだけ」で、型の保証が不十分になりやすい
- ✅ パースは「変換して安全な型のデータを返す」ので、アプリ全体の型安全性が上がる
- ✅ TypeScript では Zod、Python では Pydantic を使うと簡単に実践できる
- ✅ 信頼境界線(API受信・ファイル読み込みなど)でパースする習慣をつけることが大切
- ✅ 「入口で弾く」設計にすると、バグが早期発見できてデバッグも楽になる
最初は「バリデーションで十分では?」と感じるかもしれません。でもアプリが大きくなるにつれて、「チェックしたはずなのに型がおかしい」という問題は確実に増えてきます。そうなる前に「パース」の習慣を身につけておくと、後々のコードがグッと書きやすくなりますよ。
ぜひ今日から、外部データを受け取る部分から「パース」アプローチを試してみてください 🎉







