Python|JSONバリデーションを型安全に行う設計整理法
この記事のポイント
- JSONバリデーションの設計責務をレビュー視点で整理できる
- if文ベース実装の問題点を体系的に把握できる
- pydantic等の型安全ライブラリ導入判断を整理できる
- 実務レビューで発生しがちな典型パターンを先回りで学習できる
そもそもJSONバリデーションとは
Python開発においてJSONバリデーションは避けて通れないテーマです。API、バッチ、Webhook、外部連携など、ほとんどのサービスがJSONを受け取り、内容を検査する工程を含みます。
バリデーション処理では以下を確認します。
- 値の存在確認(必須/任意)
- 型の整合性確認(int, str, datetime 等)
- 値の制約(範囲、正規表現、許容リスト)
- 構造の整合性(ネスト、配列長など)
Pythonは動的型言語のため、dict型でパースしたJSONをそのまま使えてしまう危うさがあります。この危うさこそがレビュー設計観点での論点です。
なぜこれをレビューするのか
レビューアー視点
-
設計責務の分離状況を確認する
データ受信層とドメイン層の境界でバリデーション責務を整理しているか。 -
dict直操作の有無を確認する
dictのまま業務処理に進行していないか。 -
型安全ライブラリの適用有無を確認する
pydantic、dataclasses、TypedDict等の活用が適切に行われているか。 -
エラー通知設計を確認する
バリデーション失敗時に握り潰しにならず、障害原因が残る設計か。 -
設計の進化可能性を見極める
今後の要件変更でも耐えうる構造になっているか。
開発者視点
- dict直操作が一番手軽に実装できる
- pydantic等の学習コストを嫌う傾向がある
- 仕様確定前は柔軟性を重視してしまう
- レビューで指摘されがちな実装パターンに無自覚になりやすい
if文自力バリデーションの設計的問題
実務では今も if文でバリデーションを書く例を見かけます。コードの自由度が高いPythonでは自然発生しやすい実装です。しかし、レビューアーとしては「原始的」「脆弱」「非推奨」寄りの実装だと認識しておく必要があります。
典型的な自力バリデーションコード
# api_request_log.py
from datetime import datetime
def parse_request_log(json_dict: dict):
if "requestId" not in json_dict:
raise ValueError("requestId is required")
if not isinstance(json_dict["requestId"], int):
raise ValueError("requestId must be int")
if "requestedAt" not in json_dict:
raise ValueError("requestedAt is required")
try:
requested_at = datetime.strptime(json_dict["requestedAt"], "%Y-%m-%dT%H:%M:%S")
except Exception:
raise ValueError("requestedAt must be valid datetime format")
# 以下省略設計的に問題となる理由
-
冗長化しやすい
フィールドが10個20個と増えると膨大なif連鎖になる。 -
漏れ・書き忘れが発生しやすい
仕様変更でフィールド追加しても、検証漏れがレビューで残りやすい。 -
エラー通知粒度が粗い
どの項目が複数同時に失敗したのか把握しにくい。 -
型安全保証が担保されない
仮にisinstanceを通しても、呼び出し側は型保証を信じきれない。 -
共通化困難
汎用的な reusable validator を組みにくい。 -
レビュー時の判読性低下
仕様要件をレビューアーがコードから読み解く負荷が高まる。
❗ 結論
「設計上の構造化が弱く、コード肥大を呼び込みやすい」
レビューアーとしては「早めに型安全設計へ移行を提案するべき実装」になります。
良い実装例(型安全設計)
# api_request_log.py
from datetime import datetime
from pydantic import BaseModel, Field, IPvAnyAddress
class ApiRequestLog(BaseModel):
request_id: int = Field(..., alias="requestId")
endpoint: str
response_code: int = Field(..., alias="responseCode")
client_ip: IPvAnyAddress = Field(..., alias="clientIp")
requested_at: datetime = Field(..., alias="requestedAt")
def parse_request_log(json_dict: dict) -> ApiRequestLog:
return ApiRequestLog(**json_dict)この設計の強み
- バリデーション責任を型定義に統合
- 各フィールドに型・制約が集中管理される
- alias指定で外部API仕様と内部属性名を分離可能
- 検査失敗時に詳細なエラー内容が得られる
- IDE補完・型チェックが有効になる
- コードレビューの判読負荷が低下する
レビューアーは「pydanticが導入されているか」を設計成熟度の1つの目安として利用できます。
レビュー観点整理
レビュー現場では以下の順で観察すると設計の意図が掴みやすくなります。
- dict直操作が消せているか
- 型定義が実装全体をカバーしているか
- エラーハンドリングが握り潰しになっていないか
- 例外メッセージが障害分析に役立つ粒度になっているか
- aliasやField制約が適切に整理されているか
- バリデーション責務が別途ロジック層に漏れていないか
良くない実装例: ケース1(典型的なdict直操作)
# api_request_log.py
from datetime import datetime
def parse_request_log(json_dict: dict):
request_id = int(json_dict["requestId"])
endpoint = json_dict["endpoint"]
response_code = int(json_dict["responseCode"])
client_ip = json_dict["clientIp"]
requested_at = datetime.strptime(json_dict["requestedAt"], "%Y-%m-%dT%H:%M:%S")
return {
"request_id": request_id,
"endpoint": endpoint,
"response_code": response_code,
"client_ip": client_ip,
"requested_at": requested_at
}
@Reviewerdict操作が直書きされています。型安全保証がないため、pydantic等の導入を検討してください。
問題解説
dict形式のまま保持され型保証が消失- バリデーション有無が分散管理になる
- 呼び出し元がフィールド存在を毎回確認する羽目になる
良くない実装例: ケース2(握り潰しハンドリング)
# api_request_log.py
from datetime import datetime
def parse_request_log(json_dict: dict):
try:
request_id = int(json_dict["requestId"])
requested_at = datetime.strptime(json_dict["requestedAt"], "%Y-%m-%dT%H:%M:%S")
# 他フィールド省略
return {"request_id": request_id, "requested_at": requested_at}
except Exception:
return None
@Reviewer例外が握り潰され、障害原因の特定が困難になります。ログ記録や例外通知を必ず残してください。
問題解説
- エラー内容がサイレントに消失
- 障害分析が困難
- 呼び出し側で
Noneをチェックし続ける破綻設計を呼ぶ
改善例:設計整理版
# api_request_log.py
from datetime import datetime
from pydantic import BaseModel, Field, IPvAnyAddress, ValidationError
class ApiRequestLog(BaseModel):
request_id: int = Field(..., alias="requestId")
endpoint: str
response_code: int = Field(..., alias="responseCode")
client_ip: IPvAnyAddress = Field(..., alias="clientIp")
requested_at: datetime = Field(..., alias="requestedAt")
def parse_request_log(json_dict: dict) -> ApiRequestLog:
try:
return ApiRequestLog(**json_dict)
except ValidationError as e:
# ログ記録は省略例
raise ValueError(f"Invalid request log: {e}")障害検出可能性を設計の第一義とするのがレビューアーの基本姿勢です。
if文自力バリデーションと型安全バリデーションのレビュー比較表
| 観点 | if文自力 | 型安全バリデーション |
|---|---|---|
| 保守性 | 低い | 高い |
| 可読性 | 低い | 高い |
| 冗長化リスク | 高い | 低い |
| エラー通知粒度 | 粗い | 詳細 |
| 拡張耐性 | 低い | 高い |
| 型保証 | なし | あり |
| レビュー指摘入りやすさ | 非常に高い | ほぼ指摘されにくい |
観点チェックリスト
まとめ
PythonのJSONバリデーションは、現代レビューの中でも非常に「設計臭」が出やすい領域です。
if文自力バリデーションは設計初期のプロトタイプでは許容されても、正式実装では型安全設計への移行が設計品質向上の王道です。
pydanticを導入し、設計責務を整理することでコードの保守性・品質・レビュー効率が大幅に向上します。