Tasuke HubLearn · Solve · Grow
#Python

Pydanticを極めてAIシステムのデータ契約を設計する

型付きLLMレスポンス、階層設定、Genericモデル、TypeAdapter、dataclass_transformなどPydantic v2の高度テクニックをまとめます。

時計のアイコン23 November, 2025
TH

Tasuke Hub管理人

東証プライム市場上場企業エンジニア

情報系修士卒業後、大手IT企業にてフルスタックエンジニアとして活躍。 Webアプリケーション開発からクラウドインフラ構築まで幅広い技術に精通し、 複数のプロジェクトでリードエンジニアを担当。 技術ブログやオープンソースへの貢献を通じて、日本のIT技術コミュニティに積極的に関わっている。

🎓情報系修士🏢東証プライム上場企業💻フルスタックエンジニア📝技術ブログ執筆者

高度なPydanticを使うべき理由

  • LLMレスポンスやETL中間データをdictのまま扱うと、型崩れを実行時まで検知できない。
  • バリデーションロジックが散在し、変更時に一貫性を保てない。
  • ジェネリックなRAGパイプラインやFeature Storeでは、スキーマを再利用する仕組みが必要。

Pydantic v2はBaseModelだけでなく、TypeAdapter, field_validator, dataclass_transform, BaseSettings, GenericModelなど多彩な機能を備えています。ここでは、AIシステム向けにそれらを組み合わせるパターンを解説します。

ベストマッチ

最短で課題解決する一冊

この記事の内容と高い親和性が確認できたベストマッチです。早めにチェックしておきましょう。

TypeAdapterでLLMレスポンスを即型付け

from pydantic import BaseModel, TypeAdapter

class LLMAnswer(BaseModel):
    summary: str
    tags: list[str]
    score: float

adapter = TypeAdapter(LLMAnswer)

raw = llm_client.generate(...)
answer = adapter.validate_python(raw)

TypeAdapterBaseModelをインスタンス化せずに単体でバリデーションを行うユーティリティ。LLM出力をそのままvalidate_pythonに渡せば、フィールド不足や型不一致を即座に検知できます。

さらに理解を深める参考書

関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。

field_validatormodel_validator

class SearchQuery(BaseModel):
    text: str
    limit: int = 10

    @field_validator('limit')
    @classmethod
    def validate_limit(cls, v: int) -> int:
        if not 1 <= v <= 100:
            raise ValueError('limit must be in [1,100]')
        return v

    @model_validator(mode='after')
    def check_text_and_limit(self):
        if self.text.strip() == '' and self.limit != 1:
            raise ValueError('empty query must have limit 1')
        return self

field_validatorは単一フィールド、model_validatorはモデル全体を検証する。LLMチェーンや検索APIのガードレールに最適。

さらに理解を深める参考書

関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。

GenericModelでRAGアーティファクトを統一

from pydantic import BaseModel, Field
from pydantic.generics import GenericModel
from typing import Generic, TypeVar

T = TypeVar('T')  # ペイロード

class Artifact(GenericModel, Generic[T]):
    id: str
    payload: T
    metadata: dict[str, str] = Field(default_factory=dict)

class Passage(BaseModel):
    text: str
    source: str

class Embedding(BaseModel):
    vector: list[float]
    dimension: int

PassageArtifact = Artifact[Passage]
EmbeddingArtifact = Artifact[Embedding]

Genericモデルを使えば、Artifact[Passage]Artifact[Embedding]を同じ処理パイプラインで扱えます。mypyもpayloadの型を正しく推論。

さらに理解を深める参考書

関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。

dataclass_transformでTyped DSLを作る

dataclass_transform = pydantic.dataclasses.dataclass

@dataclass_transform
class Prompt(BaseModel):
    system: str
    user: str

@Prompt.model_validator(mode='after')
def ensure_tokens(prompt: Prompt) -> Prompt:
    if count_tokens(prompt.user) > 4096:
        raise ValueError('too long')
    return prompt

Pydantic v2のdataclassesdataclass_transform互換なので、mypyにとってもdataclassとして扱われます。

さらに理解を深める参考書

関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。

BaseSettingsで階層設定

from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_prefix='APP_')

    openai_api_key: str
    redis_url: AnyUrl
    tracing_enabled: bool = False

settings = Settings()  # envから読み込み

env_prefixで環境変数プレフィックスを強制し、設定値の抜け漏れを起動時に検知。Literalで許容値を限定することも可能。

さらに理解を深める参考書

関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。

RootModelで非構造データをラップ

class EmbeddingList(RootModel[list[list[float]]]):
    def cosine(self, idx: int, other_idx: int) -> float:
        ...

embeddings = EmbeddingList.model_validate(raw_vectors)

RootModelを使うと、リストや辞書にもメソッドを生やせます。ベクトル集合やスコア配列を型安全に扱える。

さらに理解を深める参考書

関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。

field_serializer でAPI出力を整形

class Report(BaseModel):
    created_at: datetime
    score: Decimal

    @field_serializer('created_at', when_used='json')
    def serialize_time(self, dt: datetime) -> str:
        return dt.strftime('%Y-%m-%dT%H:%M:%SZ')

LLMやREST APIの外部公開時にフォーマットを統一できる。

さらに理解を深める参考書

関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。

computed_field で派生データを自動計算

class Document(BaseModel):
    tokens: list[str]

    @computed_field(return_type=int)
    def token_count(self) -> int:
        return len(self.tokens)

テンプレートやUIに渡すときに、計算済みフィールドを含められる。

さらに理解を深める参考書

関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。

Annotated + BeforeValidator で軽量バリデータ

from typing_extensions import Annotated

PositiveFloat = Annotated[float, Field(gt=0)]

class LLMScore(BaseModel):
    score: PositiveFloat

小さな制約は Annotatedで再利用可能。

さらに理解を深める参考書

関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。

TypeAdapter で辞書→TypedDictを検証

class Metric(TypedDict):
    name: str
    value: float

metric_adapter = TypeAdapter(Metric)
metric = metric_adapter.validate_python(raw_metric)

TypedDictにはTypeAdapterを使う。ETL途中の生データを最小コストで検証できる。

さらに理解を深める参考書

関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。

カスタムエラーとユーザーフレンドリーなレスポンス

class CustomError(PydanticCustomError):
    def __init__(self, code: str, msg: str) -> None:
        super().__init__('custom_error', msg)
        self.extra = {'code': code}

class Query(BaseModel):
    q: str

    @field_validator('q')
    @classmethod
    def not_empty(cls, v: str) -> str:
        if not v:
            raise CustomError('EMPTY_QUERY', 'query must not be empty')
        return v

API層でエラーコードをそのままHTTPレスポンスに流用できる。

さらに理解を深める参考書

関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。

まとめ

  • Pydantic v2は単なるバリデータに留まらず、DSLやジェネリックモデル、設定管理まで網羅するツールキット。
  • AIシステムではTypeAdapterでLLMレスポンスを即時検証し、GenericModelでRAGやFeature Storeの契約を共通化する。
  • dataclass_transformやカスタムエラーを活用すれば、型とバリデーションを統合した堅牢なデータレイヤーが構築できる。

Pydanticを中心に据えたデータ契約を敷き、その上でLLM/ETL/設定管理を組み立てることで、Pythonでも型安全で可観測なAI基盤が実現できます。

さらに理解を深める参考書

関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。

この記事をシェア

続けて読みたい記事

編集部がピックアップした関連記事で学びを広げましょう。

#ベクターデータベース

ベクターデータベースで構築するセマンティック検索システム完全ガイド【2025年最新】

2025/8/12
#データセンター

【2025年版】サステナブルAIデータセンター戦略

2025/11/23
#Python

Astral製uvとtyでPythonワークフローを再設計する

2025/11/23
#AIOps

AIOps導入ガイド【2025年版】:AIを活用したシステム運用の自動化と効率化

2025/9/18
#生産性向上

Notion AIで業務効率を底上げする実践ガイド

2025/10/8
#Python

型安全PythonでAIコードベースを堅牢化する実装ガイド

2025/11/23