Pydanticで
複雑なJSONを
一発でValidation
Takanori Suzuki
PyCon mini Shizuoka 2026 / 2026 Feb 21
今日話すこと 
どんな課題があったか
Pydanticの基本
Pydanticで複雑なJSONをValidation
JSON SchemaからPydanticコード生成
さらなるValidtionルール
Photos
Tweets

#pyconshizu / @takanory
slides.takanory.net 

Who am I? / お前 誰よ 
Takanori Suzuki / 鈴木 たかのり ( @takanory)
BeProud 取締役 / Python Climber
PyCon JP Association 代表理事
Python Boot Camp 講師、Python mini Hack-a-thon 主催、Pythonボルダリング部 部長

PyCon JP 2026共同座長 [1] 
日程:2026年8月21日(金)〜23日(日)
会場: 広島国際会議場
共同座長:佐野浩士、鈴木たかのり
主催メンバー募集中!! 
イベント企画の具体化を進めてくれる方募集
PyCon JP 2026主催メンバー申込フォーム [2]

BeProud Inc. 
BeProud: Pythonシステム開発、コンサル
connpass: IT勉強会支援プラットフォーム
PyQ: Python独学プラットフォーム
TRACERY: システム開発ドキュメントサービス

BeProudメンバー募集中 
ブース出展してます 
どんな課題があったか 
学習教材のWebシステム [3] 
Webブラウザ上で副教材を使った勉強ができる
さまざまな解答フォーム形式 
記述、選択式、並べ替え等
さまざまな解答形式の教材を作成 
フォーム形式ごとに異なる設定項目 
記述式
表紙形式:フォーム幅
解答欄:正解、別解、プレースホルダー
選択式
表示形式:ボタンorセレクトボックス、選択肢ラベル
解答欄:選択肢リスト、正解リスト
などなど…
設定項目はJSONをDBに保存 
{
"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 
目的:誤った形式のデータの混入を防ぐ
今まではJSON SchemaでValidation
JSON Schemaとは 
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でいいんじゃないの? 
マナビリアCBTが爆誕[6] 
ワークのみ→CBT(テスト)が追加(2026年4月)
全問自動採点のCBTサービス開始: https://www.meijitosho.co.jp/info/?id=20250801
似てるけど微妙に異なるJSON仕様 
点数:CBTのみ
ヒント:ワークのみ
解答形式:共通
ソート順:共通
などなど
JSON Schemaで両方に対応する? 
共通の所は共通の処理にしたい
コピペで似たJSON Schema管理はやりたくない
JSON Schema実装のつらみ(私見) 
Schemaが長くて見づらい
定義がJSONなので読みにくい
Pythonコード中に長いdictがある
フォーム形式ごとにバリデーション切り替え
Pythonの
if文とJSON Schemaの混在
Pydanticに書き換えよう!! 
PydanticでValidationの結論 
めっちゃいい感じにできた(自画自賛)
Pydanticの基本 
Pydanticとは 
Python用のデータValidationライブラリ
dataclass、TypedDictなどをValidation可能
型ヒントを使ってルールを定義
Pydanticをインストール 
$ 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 # メールアドレス
Validating File Data: https://docs.pydantic.dev/latest/examples/files/
JSONをValidation
正しいJSONをValidation
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
nameがないageがマイナスemailがメールアドレスじゃない
{
"age": -30,
"email": "not-an-email-address"
}
エラーがめちゃ親切 
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]
エラーがめちゃ親切 
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 
複数のモデルをUnionsでまとめる 
解答フォーム形式(記述、選択式等)ごとにPydanticモデルが必要
Unions(
|)[9]を使用すると「いずれかにマッチ」ができる
複数のモデルをUnionsでまとめる 
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)
複数の解答形式のクラス構成 
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["並べ替えボディ"]
複数の解答形式のクラス構成 
解答のベースクラスを定義
"""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できてるーーーー
$ 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できる! 
JSON Schemaから
Pydanticコード生成 
SchemaからPydanticコード生成 
実際のJSON Schemaはもっと複雑
解答形式も6パターン
Pydanticのコード書くのは大変そう
datamodel-code-generator [10] 
各種データ定義からPythonのコードを生成
入力:OpenAPI、JSON Schema、YAML、GraphQL、Python辞書など
出力:Pydantic、dataclass、TypedDictなど
datamodel-code-generator [10] 
解答欄の形式ごとに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モデルができた! 
さらなるValidtionルール 
さらなるValidtionルール 
データを意味的に解釈して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
...
https://docs.pydantic.dev/latest/api/standard_library_types/#enums
数値の範囲や文字数を指定
class WrittenBody(BaseModel):
"""記述式ボディ"""
# プーレースホルダーの文字数を指定
placeholder: str = Field(..., min_length=10, max_length=50)
# 解答の最大文字数を100以下に指定
max_chars: PositiveInt = Field(..., le=100)
https://docs.pydantic.dev/latest/api/standard_library_types/#strings
https://docs.pydantic.dev/latest/api/standard_library_types/#integers
選択肢は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
つらみは解消した? 
Schemaが長くて見づらい
コンパクトになった
定義がJSONなので読みにくい
型ヒントで読みやすく
フォーム形式ごとにバリデーション切り替え
1つのモデルに集約
他にもいろいろできるので
詳しくはドキュメント読んでね 
複雑なデータをValidation
→Pydanticを検討しよう! 
Thank You 
slides.takanory.net 20260221shizuoka/code
takanory takanory takanory takanory
