2025 年 10 月: データ検証ライブラリ Pydantic の紹介¶
寺田 学@terapyonです。2025 年 10 月の「Python Monthly Topics」は、データ検証ライブラリの Pydantic を紹介します。
型安全とデータ構造¶
主題の Pydantic の説明に入る前に、Python における型安全の考え方とデータ構造についておさらいしておきます。
型安全のための型ヒント¶
Python は動的型付け言語です。型を宣言せずにコーディングすることができますが、型ヒントを書くことで型安全にコーディングできます。 最近の Python コードには型ヒントが書かれていることが多くなっているかと思います。
本連載(Python Monthly Topics)でも、過去に型ヒント関係のトピックを扱っていますので参照してください。
2022 年 9 月: Python 最新バージョン対応!より良い型ヒントの書き方
2024 年 11 月: Python 型ヒントの動向と新しい機能の紹介
私自身は、型ヒントによって、より型を意識したコーディングスタイルに変化しています。 旧来、関数やメソッドの引数や戻り値にタプルや辞書を使うことが多くありましたが、厳密に型を付けるために、TypedDict や dataclass を活用することが増えています。
型が厳密になっていることで、IDE などでの補完が充実したり、エラーの早期発見につながることも多くあります。
データ構造¶
内部の状態を管理するには、リストと辞書を組み合わせることで多くのデータ構造を表すことが可能です。 しかし、自由な構造で表現すると、前項で示した型安全なコーディングができません。 さらに、関数の引数で受け取ったデータを、毎回検証するという必要性が出てきます。
ここでは、以下のような支店名とスタッフをあらわすデータを考えていきます。
branch = {
"branch_name": "東京",
"staff": [
{"number": 1, "name": "佐藤", "is_leader": True, "leader_period": 3},
{"number": 5, "name": "田中"},
{"number": None, "name": "山本"},
],
}
注目すべきは、 staff
の中には各スタッフを表す辞書がリストの中に入っています。
スタッフの辞書に、 number
に None
が入っていたり、 is_leader
キーや leader_period
キーがない場合があります。
このデータを受け取るときは、以下のように、値を確認したり、キーが存在するかを確認したりする必要があります。
def get_user_attr(branch):
for staff in branch["staff"]:
number = staff["number"]
if number is None: # Noneの場合の条件を追加
continue
is_leader = staff.get("is_leader", False) # キーかあるかどうかを確認してデフォルト値を設定
...
このコードを型安全にするには、 TypedDict
を用いることができます。
詳細は、先にも紹介した 2022 年 9 月: Python 最新バージョン対応!より良い型ヒントの書き方 を参照してください。
しかし、TypedDict は、データの値やキーを保証していません。あくまでも、型ヒントのための宣言だからです。 TypedDict で宣言していないキーを追加することや、違うデータ型を値として入れることもできます。
そこで、データ構造を明確にするための機能として、 dataclass
があります。
先程の branch
辞書を dataclass で表現します。
from dataclasses import dataclass
@dataclass
class Staff:
"""スタッフ"""
number: int | None # 社員番号
name: str # 社員名
is_leader: bool = False # リーダー
leader_period: int = 0 # リーダー経験
@dataclass
class Branch:
"""支店"""
branch_name: str # 支店名
staff: list[Staff] # 社員リスト
この Branch
クラスをインスタンス化して使うことで、データを使う時に属性の存在を確認する必要がなくなります。
ただ、データの値については確認していません。インスタンス化する時に確認するか、dataclass 内にバリデーションのコードを追記する必要があります。
このようなデータの値を検証するといった課題に対して、Pydantic を使うと簡潔にデータ検証機能が組み込めます。
Pydantic とは¶
Pydantic は、データ検証用のパッケージです。
バージョン(執筆時点: 2025 年 10 月 20 日): 2.12.3
対応 Python バージョン: 3.9 から 3.14
Pydantic は FastAPI のデータ検証で利用されていることで注目を浴びるようになりました。また、Django Ninja という Django 用の REST フレームワークでも使われています。 Pydantic は、これらのフレームワークから独立した Python のパッケージですので、他のフレームワークで利用することや独自のスクリプトなどでも利用可能です。
Pydantic の BaseModel
を継承したクラスを書くことで、データの検証されたオブジェクトを作ることができます。
dataclass のように、型ヒント付きのクラス属性を定義することで、インスタンス化する際にデータ検証が行われます。
検証に失敗すると、 ValidationError
という専用のエラーが送出され、エラーオブジェクトの中にエラーの詳細が格納されています。
Pydantic は、独自の検証スクリプトを通さずにオブジェクトが安全に作れることが一番うれしいところです。 オブジェクトが作られれば、適切なデータ及びデータ型となっていることが保証されるため、データ受け渡し時の条件分岐などによるハンドリングが不要になります。 また、検証内容が、Python のコードとしてわかりやすく表現されることも良い点だと思います。
Pydantic の基本的な使い方¶
Pydantic を使うには pip コマンドでインストールします。
$ pip install pydantic
先程の、 Branch
クラスを Pydantic に置き換えてみます。
from pydantic import BaseModel
class Staff(BaseModel):
"""スタッフ"""
number: int | None
name: str
is_leader: bool = False
leader_period: int = 0
class Branch(BaseModel):
"""支店"""
branch_name: str
staff: list[Staff]
dataclass とほぼ同じコードになります。違いはデコレータでの宣言ではなく、 BaseModel
を継承する点です。
ここからは、JSON ファイルを入力に使い、データの検証を行います。 データ構造の項で利用した、支店名とスタッフを表す辞書を JSON 形式にしたものを準備します。 この JSON ファイルを読み込んで確認をしてみましょう。
{
"branch_name": "東京",
"staff": [
{ "number": 1, "name": "佐藤", "is_leader": true, "leader_period": 3 },
{ "number": 5, "name": "田中" },
{ "number": null, "name": "山本" }
]
}
JSON ファイルを使って Branch インスタンスを作り、生成されたオブジェクトを print() 関数で確認します。
import json
with open("branch.json", "r", encoding="utf-8") as f:
data = json.load(f)
branch = Branch(**data)
print(branch)
# branch_name='東京' staff=[
# Staff(number=1, name='佐藤', is_leader=True, leader_period=3),
# Staff(number=5, name='田中', is_leader=False, leader_period=0),
# Staff(number=None, name='山本', is_leader=False, leader_period=0)]
Branch クラスのインスタンスとして branch オブジェクトができたことがわかりました。
次に以下のように、想定しないデータ型や必要な属性がない場合の検証をしてみます。
{
"branch_name": 10,
"staff": [
{ "number": 1, "name": "佐藤", "is_leader": null, "leader_period": 3 },
{ "number": 5, "name": "田中" },
{ "name": "山本" }
]
}
この JSON ファイルには 3 つの間違いがあります。
branch_name が数値になっている
is_leader が null(None)になっている
3 番目の staff に number キーがない
import json
from pydantic import ValidationError
with open("branch-error.json", "r", encoding="utf-8") as f:
data = json.load(f)
try:
branch = Branch(**data)
except ValidationError as e:
print(e.errors())
Pydantic のモデルである、 Branch
をインスタンス化する時に、データ検証が動作します。
データ検証エラーになると ValidationError
が送出されます。
このエラーオブジェクトに .errors()
メソッドがあり、メソッドを実行すると以下のようなリストの辞書が出力されます。
.errors()
メソッドの結果は以下のようになりました。
[{'input': 10,
'loc': ('branch_name',),
'msg': 'Input should be a valid string',
'type': 'string_type',
'url': 'https://errors.pydantic.dev/2.12/v/string_type'},
{'input': None,
'loc': ('staff', 0, 'is_leader'),
'msg': 'Input should be a valid boolean',
'type': 'bool_type',
'url': 'https://errors.pydantic.dev/2.12/v/bool_type'},
{'input': {'name': '山本'},
'loc': ('staff', 2, 'number'),
'msg': 'Field required',
'type': 'missing',
'url': 'https://errors.pydantic.dev/2.12/v/missing'}]
3 つのエラーが出ていることがわかりました。
1 つ目は、 'loc': ('branch_name',)
とありますので、 branch_name
に関するエラーで、 input
に 10
となっていて、 msg
にあるように文字列型が必要となっています。
2 つ目は、 'loc': ('staff', 0, 'is_leader')
となっていて、 staff
属性のインデックス 0
の is_leader
に関するエラーで、 input
が None
であることがわかります。
3 つ目も同様に loc
, msg
, input
を確認することで詳細がわかります。
このように Pydantic は全データを検証し、検証結果をわかりやすく出力してくれます。
データ形式をより具体的にする¶
Pydantic の Field() 関数を使って、属性値をより具体的に宣言しデータ検証することができます。
例えば以下のようなことが可能です。
必須かどうか
デフォルト値の指定
数値の範囲
属性のタイトルを付記
引き続き Branch
クラスを変更していきます。
今回の変更に合わせて、 branch_name
を列挙型で候補を宣言しデータ検証をあわせて行います。
import enum
from pydantic import Field
class BRANCH_NAMES(str, enum.Enum): # 列挙型(enum)を使って宣言
"""支店名の列挙型"""
TOKYO = "東京"
OSAKA = "大阪"
FUKUOKA = "福岡"
SAPPORO = "札幌"
class Staff(BaseModel):
"""スタッフ"""
number: int | None = Field(..., title="社員番号")
name: str = Field(..., title="社員名")
is_leader: bool = Field(False, title="リーダー")
leader_period: int = Field(0, title="リーダー経験", ge=0, lt=20)
class Branch(BaseModel):
"""支店"""
branch_name: BRANCH_NAMES = Field(..., title="支店名")
staff: list[Staff] = Field([], title="社員リスト")
変更したコードを説明します。
Field()関数 の第 1 引数はデフォルト値です。
...
や False
, 0
を指定しています。
...
は、 [エリプシス]
(https://docs.python.org/ja/3.14/library/stdtypes.html#bltin-ellipsis-object) という Python の省略を表す定数を指定しています。Pydantic の Field()関数にエリプシスを渡すことで必須を示します。
title キーワード引数は、属性を表すタイトルです。
ge, lt キーワード引数は、数値などの範囲を示すことができます。ge は以上、lt は未満を示します。
branch_name のデータ型は、str から列挙型で宣言した BRANCH_NAMES
に変更しました。
続いて、以下の JSON ファイルを検証してみます。
{
"branch_name": "広島",
"staff": [
{ "number": 1, "name": "佐藤", "is_leader": null, "leader_period": 35 },
{ "number": 5, "name": "田中" },
{ "name": "山本" }
]
}
import json
from pydantic import ValidationError
with open("branch-error2.json", "r", encoding="utf-8") as f:
data = json.load(f)
try:
branch = Branch(**data)
except ValidationError as e:
print(e.errors())
実行の結果は次のとおりです。
[{'ctx': {'expected': "'東京', '大阪', '福岡' or '札幌'"},
'input': '広島',
'loc': ('branch_name',),
'msg': "Input should be '東京', '大阪', '福岡' or '札幌'",
'type': 'enum',
'url': 'https://errors.pydantic.dev/2.12/v/enum'},
{'input': None,
'loc': ('staff', 0, 'is_leader'),
'msg': 'Input should be a valid boolean',
'type': 'bool_type',
'url': 'https://errors.pydantic.dev/2.12/v/bool_type'},
{'ctx': {'lt': 20},
'input': 35,
'loc': ('staff', 0, 'leader_period'),
'msg': 'Input should be less than 20',
'type': 'less_than',
'url': 'https://errors.pydantic.dev/2.12/v/less_than'},
{'input': {'name': '山本'},
'loc': ('staff', 2, 'number'),
'msg': 'Field required',
'type': 'missing',
'url': 'https://errors.pydantic.dev/2.12/v/missing'}]
エラーメッセージの確認方法は先程とほぼ同様です。
今回は、データ検証でより詳細に確認している項目で、 ctx
(追加情報: コンテキスト)が加わっています。
'loc': ('branch_name',)
は、列挙型で 4 つの文字列以外を受け付けないようにしたので、エラーになっていることがわかります。
'loc': ('staff', 0, 'leader_period')
は、0 以上 20 未満と指定しているので、エラーとなっています。
詳細な検証とデータ変換¶
データの検証には単一の属性に対してデータ型や値が条件にあっているのかをチェックするものと、複数の属性にまたがって条件をチェックするものがあります。
ここでは、開始時間(start_time)と終了時間(end_time)を追加した JSON を用い、最初に単一属性に対する検証を解説し、続いて複数の属性にまたがる検証を説明します。
{
"branch_name": "東京",
"staff": [
{
"number": 1,
"name": "佐藤花子",
"is_leader": true,
"leader_period": 3,
"start_time": "9:00",
"end_time": "17:15"
},
{
"number": 5,
"name": "田中太郎",
"start_time": "12:00",
"end_time": "18:00"
},
{
"number": 8,
"name": "山本次郎",
"start_time": "8:00",
"end_time": "19:00"
}
]
}
検証の条件は以下のように設定します。
始時間(start_time)と終了時間(end_time)のフォーマットは
H:MM
と、:
区切りの数値であることそれぞれの時間は、30 分単位とする
開始時間は、8:00 から 11:00 の間であること
終了時間は、15:00 から 20:00 の間であること
開始時間と終了時間の間隔は 9 時間以内であること
さらに、データ検証が通過した場合は、2 つの時間を datetime.time
オブジェクトに変換します。
これらの項目のうち、「開始時間と終了時間は最大 9 時間」という条件は、開始時間と終了時間の 2 つの属性にまたがる検証になります。その他は単一の属性での検証になります。
単一の属性の検証とデータ変換¶
import datetime
from typing import Any
def _valid_half_time(v: Any) -> datetime.time:
"""30分単位の時間を検証する共通関数"""
if isinstance(v, datetime.time): # datetime.time型の場合
time_ = v # 変換せずにそのまま使う
elif isinstance(v, str): # 文字列型の場合
try:
h, m = v.split(":") # ':'で分割
except ValueError:
raise ValueError(f"{v}は ':' が1つで区切られていません")
try:
time_ = datetime.time(int(h), int(m)) # 24時間制の時間に変換
except ValueError:
raise ValueError(f"{v}を24時間制の時間に変換できません")
else:
raise ValueError(f"{v}のデータ型不正です: {type(v)}")
# 30分単位でない場合はエラー
if time_.minute not in (0, 30) or time_.second or time_.microsecond:
raise ValueError(f"{v}は30分単位の時間ではありません")
return time_
def valid_start_time(v: Any) -> datetime.time:
"""開始時間の範囲を検証する関数"""
time_ = _valid_half_time(v)
# 開始時間の範囲を検証
if not datetime.time(8, 0) <= time_ <= datetime.time(11, 0):
raise ValueError(f"{v}は指定された開始時間の範囲ではありません")
return time_
def valid_end_time(v: Any) -> datetime.time:
"""終了時間の範囲を検証する関数"""
time_ = _valid_half_time(v)
# 終了時間の範囲を検証
if not datetime.time(15, 0) <= time_ <= datetime.time(20, 0):
raise ValueError(f"{v}は指定された終了時間の範囲ではありません")
return time_
_valid_half_time()
関数は、30 分単位の時間であることを確認するための関数です。
開始時間と終了時間が同じ条件で検証するために共通で使う関数として宣言しています。
データを検証する際にどのようなデータ型が渡されてくるかわからないので、 Any
としてどのようなデータ型も受け入れるようにしています。
JSON からデータが来る場合には文字列型を想定していますが、Python 内部で利用する際に datetime.time
型で渡ってくる場合も想定して、 isinstance
でデータ型を確認して内部で挙動を変えています。
また、 str
または datetime.time
以外の場合には ValueError としています。
valid_start_time()
関数と valid_end_time()
関数は、許可されている時間の範囲が違うので個別に関数を宣言しています。
これらの関数を検証に使うために、 start_time
と end_time
に Annotated で注釈を付けています。
from typing import Annotated
from pydantic import BeforeValidator
class Staff(BaseModel):
"""スタッフ"""
number: int | None = Field(..., title="社員番号")
name: str = Field(..., title="社員名")
is_leader: bool = Field(False, title="リーダー")
leader_period: int = Field(0, title="リーダー経験", ge=0, lt=20)
start_time: Annotated[datetime.time, BeforeValidator(valid_start_time)]
end_time: Annotated[datetime.time, BeforeValidator(valid_end_time)]
ここでは、 Annotated
で、データ型と検証の方法を指定しました。
BeforeValidator
は、データ生成前に検証することを示しています。
単一属性に対する動作の確認のために、JSON ファイルを検証します。
import json
from pydantic import ValidationError
with open("branch2.json", "r", encoding="utf-8") as f:
data = json.load(f)
try:
branch = Branch(**data)
except ValidationError as e:
print(e.errors())
[{'ctx': {'error': ValueError('17:15は30分単位の時間ではありません')},
'input': '17:15',
'loc': ('staff', 0, 'end_time'),
'msg': 'Value error, 17:15は30分単位の時間ではありません',
'type': 'value_error',
'url': 'https://errors.pydantic.dev/2.12/v/value_error'},
{'ctx': {'error': ValueError('12:00は指定された開始時間の範囲ではありません')},
'input': '12:00',
'loc': ('staff', 1, 'start_time'),
'msg': 'Value error, 12:00は指定された開始時間の範囲ではありません',
'type': 'value_error',
'url': 'https://errors.pydantic.dev/2.12/v/value_error'}]
17:15 となっている部分が 30 分単位でないこと、start_time が 12:00 となっている部分が時間の範囲でないことの検証ができています。
【コラム】 Annotated パターンとデコレータパターン¶
Pydantic には単一の属性を検証する方法が 2 つ提供されています。 Annotated パターンとデコレータ(@field_validator)パターンです。
デコレータパターンは、Pydantic のバージョン 1 で提供されていた @validator
に似た方法です。
バージョン 1 からの移行コードであれば、デコレータパターンを利用するほうが良いでしょう。
新規に作る場合は、それぞれの特徴を理解したうえでどちらのパターンを使うかを決めることになります。
Annotated パターンは、型ヒントの機能を使っていることから IDE の支援を受けやすくなります。 さらに、Annotated の宣言を変数に入れることで、同じ検証を複数の Pydantic クラスで使い回すことも可能です。
デコレータパターンは、すべての属性にすべて同じ検証を入れることができたり、複数の属性にまとめて検証の設定をすることができます。
【コラム】 時間の検証と変換にカスタムバリデータを使わない方法¶
今回は 30 分刻みの時刻になるように制限があったためカスタムバリデータを作りましたが、そのような複雑な条件がなければ、以下のように簡潔に条件を書くことができます。
class Sample(BaseModel):
start_time: datetime.time = Field(
..., ge=datetime.time(8, 0), le=datetime.time(11, 0)
)
これは、Pydantic 内部でデータ型の自動変換行われ、さらに範囲を指定した検証を行うことができるからです。
今回は、 datetime.time
の例を示しましたが、 int
の場合においても、変換可能な "10"
のような文字列を int 型に自動変換をしてくれます。
複数の属性にまたがる検証¶
ここからは最後の条件である、start_time と end_time が 9 時間を超えていないことを検証します。
from typing import Self
from pydantic import model_validator
class Staff(BaseModel):
"""スタッフ"""
number: int | None = Field(..., title="社員番号")
name: str = Field(..., title="社員名")
is_leader: bool = Field(False, title="リーダー")
leader_period: int = Field(0, title="リーダー経験", ge=0, lt=20)
start_time: Annotated[datetime.time, BeforeValidator(valid_start_time)]
end_time: Annotated[datetime.time, BeforeValidator(valid_end_time)]
@model_validator(mode="after")
def check_duration(self) -> Self:
"""時間間隔を検証"""
today = datetime.datetime.today().date()
# 開始時間と終了時間を比較するために datetime.datetime型に変換
start_dt = datetime.datetime.combine(today, self.start_time)
end_dt = datetime.datetime.combine(today, self.end_time)
# 9時間を超えていないかを確認
if (end_dt - start_dt) > datetime.timedelta(hours=9):
raise ValueError("開始時間と終了時間が9時間を超えています")
return self # 検証に通過した場合は、インスタンスを返す
インスタンスメソッド check_duration()
を宣言します。
複数の属性にまたがるデータ検証を行いたい場合は @model_validator
デコレータを使います。
インスタンスができた後にデータ検証が実行されるように、 mode="after"
と指定しています。
検証の結果は以下のようになります。
[{'ctx': {'error': ValueError('17:15は30分単位の時間ではありません')},
'input': '17:15',
'loc': ('staff', 0, 'end_time'),
'msg': 'Value error, 17:15は30分単位の時間ではありません',
'type': 'value_error',
'url': 'https://errors.pydantic.dev/2.12/v/value_error'},
{'ctx': {'error': ValueError('12:00は指定された開始時間の範囲ではありません')},
'input': '12:00',
'loc': ('staff', 1, 'start_time'),
'msg': 'Value error, 12:00は指定された開始時間の範囲ではありません',
'type': 'value_error',
'url': 'https://errors.pydantic.dev/2.12/v/value_error'},
{'ctx': {'error': ValueError('開始時間と終了時間が9時間を超えています')},
'input': {'end_time': '19:00',
'name': '山本次郎',
'number': 8,
'start_time': '8:00'},
'loc': ('staff', 2),
'msg': 'Value error, 開始時間と終了時間が9時間を超えています',
'type': 'value_error',
'url': 'https://errors.pydantic.dev/2.12/v/value_error'}]
9 時間を超えるパターンの検証ができました。
単一属性に関する検証や複数の属性にまたがる検証ともに、Python のコードで書いていますので、アイデア次第では複雑な検証や外部の検証ツールを使うことも可能になります。
補足情報¶
JSONSchema からの変換¶
既存の実装で JSONSchema
を用いた検証を実施している場合があると思います。
その際に Pydantic でのデータ検証に置き換えたい場合にサードパーティ製ライブラリで JSONSchema を Pydantic のコードに変換することができます。
datamodel-code-generator を使うと様々な入力に対して、Pydantic などのモデルファイルを生成できます。 このツールの作者は、PyCon JP 2025 で Python3.14 の新機能の紹介 の招待講演を行った、 青野高大 さんです。
入力フォーマットは、 OpenAPI 3、 JSON Schema、JSON/YAML/CSV Data、 Python の辞書、 GraphQL schema とさまざまなものに対応しています。
出力フォーマットは、 pydantic.BaseModel、 pydantic_v2.BaseModel、 dataclasses.dataclass、 typing.TypedDict、 msgspec.Struct に対応し、さらに、 jinja2 template で宣言するカスタムタイプにも対応しています。
既に存在する、JSONSchema などの仕様や検証ツールを、Python のコードに置き換えることができます。 それにより、Python で仕様と実装の一元管理ができ、Python でコーディングする際に IDE での補完が充実し、型ヒントの恩恵が得られるという大きなメリットが得られます。
詳細は公式ドキュメントを確認してください。
TypedDict / dataclass との使い分け¶
今回は Pydantic を紹介しましたが、データ構造やデータモデルを作るときに、TypedDict や dataclass を用いる方法もあります。
TypedDict は、辞書を使ったデータ構造に型ヒントを追加するというアプローチになります。 また、dataclass は、型ヒントを使ったデータモデルを作るものです。
Python の標準に取り込まれている機能になりますが、どちらの方法もデータ検証をサポートしていません。 また、TypedDict は、あくまで型ヒントを付与しているだけなので、キーの存在が保証されているものではありません。dataclass は属性が決まっているので、TypedDict に比べると構造が明確になっています。
ここでは、筆者の使い分けの基準を示します。
辞書でデータ構造が存在している場合は TypedDict で型ヒントを付与
改造やデータ構造が複雑になる場合は dataclass への置き換えを検討
新規にデータ構造を示すのであれば、dataclass でデータモデル化
Python コードの内部で利用する場合は、dataclass を利用
外部(Web API など)からのデータを使う場合はデータ検証を重視し Pydantic を採用
まとめ¶
今回は、Pydantic を紹介しました。Pydantic はデータ検証を担うサードパーティ製ライブラリです。 型ヒントを使い、明確なデータ構造を示すことができます。 内部で自動的にデータ変換が行われたりと便利な機能が備わっています。
また、データの検証だけはなく、データの構造を明確にすることができることから、Python コードをスッキリとスマートに書くことができるライブラリであると考えています。
みなさんもデータ検証やデータ構造の見直しに Pydantic を使ってみてください。
最後に今回のコードすべてを掲載しておきます。
import datetime
import enum
from typing import Annotated, Any, Self
from pydantic import BaseModel, Field, BeforeValidator, model_validator
def _valid_half_time(v: Any) -> datetime.time:
"""30分単位の時間を検証する共通関数"""
if isinstance(v, datetime.time): # datetime.time型の場合
time_ = v # 変換せずにそのまま使う
elif isinstance(v, str): # 文字列型の場合
try:
h, m = v.split(":") # ':'で分割
except ValueError:
raise ValueError(f"{v}は ':' が1つで区切られていません")
try:
time_ = datetime.time(int(h), int(m)) # 24時間制の時間に変換
except ValueError:
raise ValueError(f"{v}を24時間制の時間に変換できません")
else:
raise ValueError(f"{v}のデータ型不正です: {type(v)}")
# 30分単位でない場合はエラー
if time_.minute not in (0, 30) or time_.second or time_.microsecond:
raise ValueError(f"{v}は30分単位の時間ではありません")
return time_
def valid_start_time(v: Any) -> datetime.time:
"""開始時間の範囲を検証する関数"""
time_ = _valid_half_time(v)
# 開始時間の範囲を検証
if not datetime.time(8, 0) <= time_ <= datetime.time(11, 0):
raise ValueError(f"{v}は指定された開始時間の範囲ではありません")
return time_
def valid_end_time(v: Any) -> datetime.time:
"""終了時間の範囲を検証する関数"""
time_ = _valid_half_time(v)
# 終了時間の範囲を検証
if not datetime.time(15, 0) <= time_ <= datetime.time(20, 0):
raise ValueError(f"{v}は指定された終了時間の範囲ではありません")
return time_
class BRANCH_NAMES(str, enum.Enum):
"""支店名の列挙型"""
TOKYO = "東京"
OSAKA = "大阪"
FUKUOKA = "福岡"
SAPPORO = "札幌"
class Staff(BaseModel):
"""スタッフ"""
number: int | None = Field(..., title="社員番号")
name: str = Field(..., title="社員名")
is_leader: bool = Field(False, title="リーダー")
leader_period: int = Field(0, title="リーダー経験", ge=0, lt=20)
start_time: Annotated[datetime.time, BeforeValidator(valid_start_time)]
end_time: Annotated[datetime.time, BeforeValidator(valid_end_time)]
@model_validator(mode="after")
def check_duration(self) -> Self:
"""時間間隔を検証"""
today = datetime.datetime.today().date()
# 開始時間と終了時間を比較するために datetime.datetime型に変換
start_dt = datetime.datetime.combine(today, self.start_time)
end_dt = datetime.datetime.combine(today, self.end_time)
# 9時間を超えていないかを確認
if (end_dt - start_dt) > datetime.timedelta(hours=9):
raise ValueError("開始時間と終了時間が9時間を超えています")
return self # 検証に通過した場合は、インスタンスを返す
class Branch(BaseModel):
"""支店"""
branch_name: BRANCH_NAMES = Field(..., title="支店名")
staff: list[Staff] = Field([], title="社員リスト")
if __name__ == "__main__":
import json
from pprint import pprint
from pydantic import ValidationError
with open("branch2.json", "r", encoding="utf-8") as f:
data = json.load(f)
try:
branch = Branch(**data)
except ValidationError as e:
pprint(e.errors())