Pydantic
複雑なJSON
一発でValidation

../_images/pycon-mini-shizuoka-logo.png

Takanori Suzuki

PyCon mini Shizuoka 2026 / 2026 Feb 21

今日話すこと nesshou

  • どんな課題があったか yabai

  • Pydanticの基本 benkyou

  • Pydanticで複雑なJSONValidation work-moeru

  • JSON SchemaからPydanticコード生成 kitai

  • さらなるValidtionルール megane

Photos camera Tweets niwatori come-on

#pyconshizu / @takanory

slides.takanory.net miru

slides.takanory.net

Who am I? / お前 誰よ beer

takanory profile kuro-chan and kuri-chan

PyCon JP 2026共同座長 [1] nakayoshi

  • 日程:2026年8月21日(金)〜23日(日)

  • 会場: 広島国際会議場

  • 共同座長:佐野浩士、鈴木たかのり

../_images/pyconjp2026-chairs1.png

主催メンバー募集中!! kamon

  • イベント企画の具体化を進めてくれる方募集

  • PyCon JP 2026主催メンバー申込フォーム [2]

BeProud Inc. work

  • BeProud: Pythonシステム開発、コンサル

  • connpass: IT勉強会支援プラットフォーム

  • PyQ: Python独学プラットフォーム

  • TRACERY: システム開発ドキュメントサービス

BeProud logos

BeProudメンバー募集中 kamon

Pythno求人のQRコード カジュアル面談のQRコード

ブース出展してます banban

../_images/beproud-booth2.jpg

どんな課題があったか yabai

学習教材のWebシステム [3] benkyou

デジタル教材マナビリア
  • Webブラウザ上で副教材を使った勉強ができる

さまざまな解答フォーム形式 good

  • 記述、選択式、並べ替え等

さまざまな解答形式

さまざまな解答形式の教材を作成 work

../_images/edit_quiz.gif

フォーム形式ごとに異なる設定項目 guruguru

  • 記述式

    • 表紙形式:フォーム幅

    • 解答欄:正解、別解、プレースホルダー

  • 選択式

    • 表示形式:ボタンorセレクトボックス、選択肢ラベル

    • 解答欄:選択肢リスト、正解リスト

  • などなど…

設定項目はJSONをDBに保存 mask

{
    "question": "Python 3.14の新機能はどれ?(選択式)"
    "answer_format": "choices",
    "display": {"choices_selector": "button",
                "choices_label": "ABC"},
    "body": {
        "answers": [
            {"answer": "t-string",
             "is_correct": true},
            {"answer": "safe external debugger",
             "is_correct": true},
            {"answer": "lazy import",
             "is_correct": false},
    ]}
}

DB保存時にJSONをValidation ok

  • 目的:誤った形式のデータの混入を防ぐ

  • 今まではJSON SchemaでValidation

JSON Schemaとは itabasami

JSON Schema logo [4]

  • JSONデータの構造をJSONで定義する言語

  • Pythonのライブラリ(jsonschema)あり

JSON Schemaのサンプル [5]

{"productId": 5, "productName": "MANAVIRIA"}
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://example.com/product.schema.json",
  "title": "Product",
  "description": "A product from Acme's catalog",
  "type": "object",
  "properties": {
    "productId": {
      "description": "The unique identifier for a product",
      "type": "integer"
    },
    "productName": {
      "description": "Name of the product",
      "type": "string"
    }
  }
}

JSON Schemaでいいんじゃないの? hate

マナビリアCBTが爆誕[6] chudoon

  • ワークのみ→CBT(テスト)が追加(2026年4月)

https://www.meijitosho.co.jp/db/info/20250801_2.png

似てるけど微妙に異なるJSON仕様 ase

  • 点数:CBTのみ

  • ヒント:ワークのみ

  • 解答形式:共通

  • ソート順:共通

  • などなど

JSON Schemaで両方に対応する? yabai

  • 共通の所は共通の処理にしたい

  • コピペで似たJSON Schema管理はやりたくない

JSON Schema実装のつらみ(私見) pusupusu

  • Schemaが長くて見づらい

  • 定義がJSONなので読みにくい

    • Pythonコード中に長いdictがある

  • フォーム形式ごとにバリデーション切り替え

    • Pythonのif文とJSON Schemaの混在

Pydanticに書き換えよう!! sore

PydanticでValidationの結論 doya

  • めっちゃいい感じにできた(自画自賛)

Pydanticの基本 benkyou

Pydanticとは mita

Pydantic logo [7]

  • Python用のデータValidationライブラリ

  • dataclass、TypedDictなどをValidation可能

  • 型ヒントを使ってルールを定義 yoshi

Pydanticをインストール kochira

$ pip install "pydantic"
$ pip install "pydantic[email]"  # email Validationする場合

JSONをValidation[8]

{
    "name": "John Doe",
    "age": 30,
    "email": "john@example.com"
}
from pydantic import BaseModel, EmailStr, PositiveInt

class Person(BaseModel):  # BaseModelを継承
    name: str
    age: PositiveInt  # 正の整数
    email: EmailStr  # メールアドレス

JSONをValidation

  • 正しいJSONをValidation yoshi

from pathlib import Path
from example_model import Person

json_string = Path('person.json').read_text()
person = Person.model_validate_json(json_string)
print(person)
#> name='John Doe' age=30 email='john@example.com'

JSONをValidation

  • 正しくないJSONをValidation ng

    • nameがない

    • ageがマイナス

    • emailがメールアドレスじゃない

{
    "age": -30,
    "email": "not-an-email-address"
}

エラーがめちゃ親切 dai-kansha

from pydantic import ValidationError

json_string = Path("person_wrong.json").read_text()
try:
    person = Person.model_validate_json(json_string)
except ValidationError as err:
    print(err)
name
  Field required [type=missing, input_value={'age': -30, 'email': 'not-an-email-address'}, input_type=dict]
    For further information visit https://errors.pydantic.dev/2.12/v/missing
age
  Input should be greater than 0 [type=greater_than, input_value=-30, input_type=int]
    For further information visit https://errors.pydantic.dev/2.12/v/greater_than
email
  value is not a valid email address: An email address must have an @-sign. [type=value_error, input_value='not-an-email-address', input_type=str]

エラーがめちゃ親切 dai-kansha

name
  Field required [type=missing, input_value={'age': -30, 'email': 'not-an-email-address'}, input_type=dict]
    For further information visit https://errors.pydantic.dev/2.12/v/missing
age
  Input should be greater than 0 [type=greater_than, input_value=-30, input_type=int]
    For further information visit https://errors.pydantic.dev/2.12/v/greater_than
email
  value is not a valid email address: An email address must have an @-sign. [type=value_error, input_value='not-an-email-address', input_type=str]
  • nameは必須のフィールド

  • ageは0より大きい

  • emailの値がメールアドレス形式じゃない

Pydanticで複雑なJSON
Validation work-moeru

複数のモデルをUnionsでまとめる nakayoshi

  • 解答フォーム形式(記述、選択式等)ごとにPydanticモデルが必要

  • Unions|[9]を使用すると「いずれかにマッチ」ができる

複数のモデルをUnionsでまとめる nakayoshi

from typing import Literal
from pydantic import BaseModel, Field

class Cat(BaseModel):
    pet_type: Literal['cat']
    meows: int

class Dog(BaseModel):
    pet_type: Literal['dog']
    barks: float

class Model(BaseModel):  # pet_typeで見分ける
    pet: Cat | Dog = Field(discriminator='pet_type')

print(Model(pet={'pet_type': 'dog', 'barks': 3.14}))
#> pet=Dog(pet_type='dog', barks=3.14)

複数の解答形式のクラス構成 purupuru

        classDiagram
    BaseForm
    BaseForm <|-- BaseWork
    BaseForm <|-- BaseCBT
    BaseWork <|-- WorkWritten
    BaseWork <|-- WorkChoices
    BaseWork <|-- WorkSorting
    BaseCBT <|-- CBTWritten
    BaseCBT <|-- CBTChoices
    BaseCBT <|-- CBTSorting
    CBTWritten *-- BaseWrittenBody
    WorkWritten *-- BaseWrittenBody
    CBTChoices *-- BaseChoicesBody
    WorkChoices *-- BaseChoicesBody
    CBTSorting *-- BaseSortingBody
    WorkSorting *-- BaseSortingBody

    namespace Work {
        class WorkWritten
        class WorkChoices
        class WorkSorting
    }

    namespace CBT {
        class CBTWritten
        class CBTChoices
        class CBTSorting
    }

    class BaseForm["解答ベースクラス"] {
        str 問題文
        int ソート順
        int 解答形式
        object 解答欄ボディ
    }
    
    class BaseCBT["CBT解答ベースクラス"] {
        int 点数
    }

    class BaseWork["ワーク解答ベースクラス"]

    class CBTWritten["CBT記述式"] {
        int 解答形式_1
        object 記述式ボディ
    }

    class CBTChoices["CBT選択式"] {
        int 解答形式_2
        object 選択式ボディ
    }

    class WorkChoices["ワーク選択式"] {
        int 解答形式_2
        object 選択式ボディ
    }

    class WorkWritten["ワーク記述式"] {
        int 解答形式_1
        object 記述式ボディ
    }

    class BaseWrittenBody["記述式ボディ"] {
        int フォームの幅
        str 正答
        str プレースホルダー
        int 最大文字数
    }

    class BaseChoicesBody["選択式ボディ"] {
        int ラジオorリストボックス
        int レイアウト
        list[str] 選択肢
        list[bool] 正解or不正解
    }
    class CBTSorting["CBT並べ替え"] {
        int 解答形式_3
        object 並べ替えボディ
    }    

    class WorkSorting["ワーク並べ替え"] {
        int 解答形式_3
        object 並べ替えボディ
    }    

    class BaseSortingBody["並べ替えボディ"]

    

複数の解答形式のクラス構成 purupuru

  • 解答のベースクラスを定義

"""pydanticで複数のモデルをUnionしていい感じに処理できるか試す"""
from typing import Literal

from pydantic import BaseModel, Field, PositiveInt

class BaseForm(BaseModel):
    """解答ベースクラス"""
    question: str  # 問題文
    sort_order: int  # ソート順
    answer_format: int  # 解答形式
    body: object  # 解答欄ボディ


class CBTForm(BaseForm):
    """CBT解答ベースクラス"""
    score: PositiveInt  # 点数
    

class WorkForm(BaseForm):
    """ワーク解答ベースクラス"""
    pass    
  • CBT記述式ワーク記述式のモデル定義

class WrittenBody(BaseModel):
    """記述式ボディ"""
    form_width: int  # フォームの幅
    answer: str  # 正答
    placeholder: str  # プレースホルダー
    max_chars: PositiveInt  # 最大文字数

    
class CBTWritten(CBTForm):
    """CBT記述式"""
    answer_format: Literal[1]  # 解答形式は「記述式」
    body: WrittenBody  # 記述式ボディ


class WorkWritten(WorkForm):
    """ワーク記述式"""
    answer_format: Literal[1]  # 解答形式は「記述式」
    body: WrittenBody  # 記述式ボディ
  • CBT選択式ワーク選択式のモデル定義

class ChoicesBody(BaseModel):
    """選択式ボディ"""
    choices_selector: int  # ラジオ or セレクトボックス
    layout: str  # レイアウト
    choices: list[str]  # 選択肢
    is_collects: list[bool]  # 正解 or 不正解


class CBTChoices(CBTForm):
    """CBT選択式"""
    answer_format: Literal[2]  # 解答形式は「選択式」
    body: ChoicesBody  # 選択式ボディ


class WorkChoices(WorkForm):
    """ワーク選択式"""
    answer_format: Literal[2]  # 解答形式は「選択式」
    body: ChoicesBody  # 選択式ボディ
  • CBT並べ替えワーク並べ替えのモデル定義

class SortingBody(BaseModel):
    """並べ替えボディ"""
    ...


class CBTSorting(CBTForm):
    """CBT並べ替え"""
    answer_format: Literal[3]  # 解答形式は「並べ替え式」
    body: SortingBody  # 並べ替えボディ

    
class WorkSorting(WorkForm):
    """ワーク並べ替え"""
    answer_format: Literal[3]  # 解答形式は「並べ替え式」
    body: SortingBody  # 並べ替えボディ
  • Unionsで複数の解答欄を1つにまとめる

class CBTAnswer(BaseModel):
    """いずれかのCBTの解答形式にマッチするモデル"""
    form: CBTWritten | CBTChoices | CBTSorting \
        = Field(discriminator="answer_format")


class WorkAnswer(BaseModel):
    """いずれかのワークの解答形式にマッチするモデル"""
    form: WorkWritten | WorkChoices | WorkSorting \
        = Field(discriminator="answer_format")
  • CBT記述式をValidation


written_sample = {  # CBT記述式のサンプル
    "score": 5,  # 点数
    "question": "Pythonの作者は?", # 採点形式
    "sort_order": 1,  # ソート順
    "answer_format": 1, # 記述式
    "body": {
        "form_width": 1,  # フォームの幅
        "answer": "Guido van Rossum",   # 正答
        "placeholder": "作者名をアルファベットで書いてください",
        "max_chars": 100, # 最大文字数
    },
}

result = CBTAnswer(form=written_sample)
print(result)
  • きちんとValidationできてるーーーー big-love

$ python unions_form.py  # 見やすくするために改行を入れている
form=CBTWritten(
  question='Pythonの作者は?',
  sort_order=1,
  answer_format=1,
  body=WrittenBody(
    form_width=1,
    answer='Guido van Rossum',
    placeholder='作者名をアルファベットで書いてください',
    max_chars=100
  ),
  score=5
)
  • ワーク選択式をValidation

choices_sample = {  # ワーク選択式のサンプル
    "question": "Python 3.14の新機能はどれ?",
    "sort_order": 2,  # ソート順
    "answer_format": 2, # 選択式
    "body": {
        "choices_selector": 1,  # ラジオ
        "layout": "default",  # レイアウト
        "choices": ["t-string", "safe external debugger", "lazy import"],
        "is_collects": [True, True, False],
    },
}

result = WorkAnswer(form=choices_sample)
print(result)
# form=WorkChoices(question='Python 3.14の新機能はどれ?', sort_order=2, answer_format=2, body=ChoicesBody(choices_selector=1, layout='default', choices=['t-string', 'safe external debugger', 'lazy import'], is_collects=[True, True, False]))

Pydanticで一発でValidationできる! kitakitakitakita-kitakitsune

JSON Schemaから
Pydanticコード生成 kitai

SchemaからPydanticコード生成 kitai

  • 実際のJSON Schemaはもっと複雑

  • 解答形式も6パターン

  • Pydanticのコード書くのは大変そう ase

datamodel-code-generator [10] kami

  • 各種データ定義からPythonのコードを生成

  • 入力:OpenAPI、JSON Schema、YAML、GraphQL、Python辞書など

  • 出力:Pydantic、dataclass、TypedDictなど

datamodel-code-generator [10] kami

  • 解答欄の形式ごとにJSONファイルを作成

  • →Pydanticのモデルコードを生成

$ pip install datamodel-code-generator
$ datamodel-codegen --input schema.json \
  --input-file-type jsonschema \
  --output-model-type pydantic_v2.BaseModel \
  --output model.py

解答欄のPydanticモデルができた! dai-kansha

さらなるValidtionルール megane

さらなるValidtionルール megane

  • データを意味的に解釈してValidationしたい

  • 複数の項目の組み合わせでValidationしたい

  • Constraints追加、Validator作成

任意の値のみ選択可能にする

  • Enumで定義した値のみ指定可 [11]

from enum import Enum

class TextInputFormat(Enum):
    """記述式のテキスト入力形式"""
    HALF_WIDTH = 1  # 幅50%
    FULL_WIDTH = 2  # 幅100%(1行)
	
class WrittenBody(BaseModel):
    """記述式ボディ"""
    text_input_format: TextInputFormat
    ...

数値の範囲文字数を指定

  • Field()クラスの引数で条件を指定

  • 文字数[12]、数値の範囲[13]など

class WrittenBody(BaseModel):
    """記述式ボディ"""
    # プーレースホルダーの文字数を指定
    placeholder: str = Field(..., min_length=10, max_length=50)
    # 解答の最大文字数を100以下に指定
    max_chars: PositiveInt = Field(..., le=100)

選択肢は1つ以上の正解が必要

  • @model_validatorでValidatorを定義 [14]

class ChoicesBody(BaseModel):
    "選択式ボディ"""
    is_collects: list[bool]  # 正解 or 不正解
	
    @model_validator(mode="after")
    def at_least_one_correct(self) -> Self:
        """is_collectsに1つ以上のTrueがあるか"""
        if not any(is_corrects):
            raise ValueError("正解の選択肢がありません")
        return self

つらみは解消した? singi-chu

  • Schemaが長くて見づらい

    • コンパクトになった

  • 定義がJSONなので読みにくい

    • 型ヒントで読みやすく

  • フォーム形式ごとにバリデーション切り替え

    • 1つのモデルに集約

他にもいろいろできるので
詳しくはドキュメント読んでね holiday-nya2

docs.pydantic.dev

複雑なデータをValidation
Pydanticを検討しよう! kyapi

Thank You pray

slides.takanory.net 20260221shizuoka/code

takanory takanory takanory takanory

takanory profile kuro-chan and kuri-chan