Parse, don’t validate by Alexis King
プログラムを実装していて、いたるところで値のバリデーションやチェックをしていると感じたことはないだろうか。
そして、氾濫するバリデーションをどうやって整理したものか頭を抱えたことはないだろうか。
そんな悩みに、この記事は方針を示してくれる。一度読んでおきたい記事だ。
validateとは値をチェックすること。 parseとはvalidateしてより制約の強い型に変換すること。
validateだけでは情報をロスしてしまい、勿体無い。parseして型に落とす。理想的には後続処理に渡す前に。そうすることで、後続の処理で同じチェックを何度もする必要がなくなる。
記事ではより詳しい説明と、実践的なアドバイスも書かれている。
現実との折り合い
ただし、すべてを「parseして型に落とす」で綺麗に解決できるわけではない。
Can types replace validation? もあわせて読むと、現実のアプリケーションでは別の都合もあることが分かる。
たとえば、ユーザー入力では複数のエラーをまとめて表示したいことがある。言語の型システムだけでは表現しにくい制約もある。外部ライブラリやフレームワークとの境界で、理想的なドメイン型だけを扱えないこともある。
TypeScript や Python で考える
現実的には、言語に搭載されている機能だけに頼るのではなく、ある程度フレームワークを活用するのが良いと思う。TypeScript なら Zod、Python なら Pydanticのようなライブラリを使う。
Pydanticでは、標準的に複数のバリデーションエラーを一度に取得できる:
#!/usr/bin/env -S uv run
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "pydantic[email]>=2.11",
# ]
# ///
from pydantic import (
BaseModel,
EmailStr,
Field,
ValidationError,
)
class AdultUser(BaseModel):
name: str = Field(min_length=3)
age: int = Field(ge=18)
email: EmailStr
try:
AdultUser(
**{
"name": "a",
"age": 15,
"email": "xxx",
}
)
except ValidationError as e:
print("❌ Validation failed\n")
for error in e.errors():
path = ".".join(str(x) for x in error["loc"])
print(f"• {path}")
print(f" 入力値 : {error.get('input')!r}")
print(f" 理由 : {error['msg']}")
print()main.py
実行結果:
❯ uv run main.py
❌ Validation failed
• name
入力値 : 'a'
理由 : String should have at least 3 characters
• age
入力値 : 15
理由 : Input should be greater than or equal to 18
• email
入力値 : 'xxx'
理由 : value is not a valid email address: An email address must have an @-sign.
一度のバリデーションで複数のエラーを取得できるので、ユーザーに対してまとめてエラーを表示することも容易だ。
その他にもPydanticには豊富な機能が存在するので、選択肢として覚えておきたい。