この記事のポイント

  • 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")

    # 以下省略

設計的に問題となる理由

  1. 冗長化しやすい
    フィールドが10個20個と増えると膨大な if 連鎖になる。

  2. 漏れ・書き忘れが発生しやすい
    仕様変更でフィールド追加しても、検証漏れがレビューで残りやすい。

  3. エラー通知粒度が粗い
    どの項目が複数同時に失敗したのか把握しにくい。

  4. 型安全保証が担保されない
    仮に isinstance を通しても、呼び出し側は型保証を信じきれない。

  5. 共通化困難
    汎用的な reusable validator を組みにくい。

  6. レビュー時の判読性低下
    仕様要件をレビューアーがコードから読み解く負荷が高まる。

❗ 結論

「設計上の構造化が弱く、コード肥大を呼び込みやすい」

レビューアーとしては「早めに型安全設計へ移行を提案するべき実装」になります。

良い実装例(型安全設計)

バリデーション責務を型安全に統合した実装
# 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直操作)

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
    }
@Reviewer
dict操作が直書きされています。型安全保証がないため、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を導入し、設計責務を整理することでコードの保守性・品質・レビュー効率が大幅に向上します。