はじまり
くそう、こんがらがってきやがった・・・
クロージャを使ってバリデーションチェックですね。
PydanticのBaseModelのバリデーションをする
Pythonには、Pydanticという強力な型アノテーションおよび値のバリデーションを実装することが出来るライブラリがあります。Python標準パッケージの中に「dataclasses」という似たような型ヒント用のライブラリもありますが、Pydanticの方がもっと強力に例外を発生することが出来るので、堅牢なコードを書くことが出来ます。僕個人的には、Pydanticを導入したらコーディング中に型の不整合によってハマることは無くなりましたし、テストコードも使う以前よりも書きやすくなりました。
そして今回は、そのPydanticの機能を利用する上で最も使うであろうクラスの「BaseModel」クラスに対する型アノテーションと値のバリデーションチェックを実装するまでの流れを紹介していきます。
ちなみに今回作ったコードは、以下のNotionのデータベースから取得したデータをAPIを介して渡したりリクエストするために作っていたものです。なので「NotionTasks」とかいうクラス名になっているわけです。
それでは行ってみましょう。
「クラス」メソッドと「インスタンス」メソッドの分別を付ける。
まあ、まずはChatGPTに質問してみるわけです。「Pydanticでバリデーションチェックをするコードを書いて。」と。
from pydantic import BaseModel, validator, ValidationError
class MyModel(BaseModel):
name: str = "default_name" # 任意フィールド
@validator("name", pre=True, always=True)
def check_not_none(cls, v):
if v is None:
raise ValueError("name cannot be None")
return v
# OK
model = MyModel(name="John")
print(model)
# Error: name cannot be None
try:
model = MyModel(name=None)
except ValidationError as e:
print(e)
そして疑問に思うわけです。
このpre
とalways
って・・・何だ?
そこでChatGPTに聞いてみるわけです。「『@validator(“name”, pre=True, always=True)』の処理の内容を教えて。」と。そして、返ってきた回答を意訳するとこんな感じでした。
pre=True
は、バリデーションが「前処理の段階」で実行されることを意味します。
always=True
は、フィールドに値が提供されていなくても、常にバリデーションを実行します。Pydanticでは、バリデーションには 前処理(pre-processing) と 後処理(post-processing) があります。
pre=True
を設定すると、フィールドの型が適用される「前」にバリデーションが実行されます。また、always=True
を設定すると、「入力がない場合にデフォルト値を使うが、そのデフォルト値もバリデーションを通したい」といったケースをカバーできます。
なるほど、クラスのフィールドに入る前と入る後の2種類のタイミングでバリデーションを評価することが出来るわけだ。すごいなPydantic。
この2つのモードでバリデーションをすれば、BaseModel
に代入する前にはデータベース(PostgreSQLなど)の中でnot nullで定義したカラムが何らかの理由でNoneになった場合に例外を出すことが出来ます。(pre=True
)
そして、BaseModel
に代入した後にはデフォルト値を定義しておくことで、必ず代入しなければならないフィールドに代入されていないことを検知できて、値自体の評価をすることも可能です。(always=True
)
よし、実装してみよう。
そして、渡されたコードを少しいじって動かしてみるわけです。ーーーしかし動かないのです。えっ・・・?
from pydantic import BaseModel, validator, ValidationError
class MyModel(BaseModel):
name: str = "default_name" # 任意フィールド
@validator("name", pre=True, always=True)
def check_not_none(self, v):
if v is None:
raise ValueError("name cannot be None")
return v
# OK
model = MyModel(name="John")
print(model)
# Error: name cannot be None
try:
model = MyModel(name=None)
except ValidationError as e:
print(e)
エラーメッセージの抜粋。
pydantic.errors.PydanticUserError: @field_validator cannot be applied to instance methods
Positive prompt: Anime style, high quality, rough, (from above, upper body), 1 young woman with brown hairr, white eyes, Stinking eyes, wearing sports wear, sadness, under the blue sky
negative prompt: violence, sexual, derogatory, toxic
(generated by Imagen 3)
あれっ・・・、なんかそれっぽいイイ感じの回答が返ってきてたのに・・・。
いいえ、違います。これは僕がいじった内容に問題がありました。
これは、@classmethod
を入れて、メソッドの第一引数にcls
を指定したら直ります。なぜなら、Pythonで定義するクラスでは、そのクラスのメソッドの第一引数にcls
を指定するとクラスメソッド、self
を指定するとインスタンスメソッドになるんですよねそう言えば。
なので、インスタンスメソッドではなくクラスメソッドとしてバリデーション用のメソッドを実行すれば問題なく通るわけです。(この例外はそれなりにPythonを使ってないと気付かなそう。)
# ↓ class method
@validator("name", pre=True, always=True)
def check_not_none(cls, v):
# ↓ instance method
@validator("name", pre=True, always=True)
def check_not_none(self, v):
@validatorデコレータは非推奨。
それじゃあ、cls
をself
で指定して、そんでもって@validator
を使った実装でハイッ解決。 とはなりませんでした。
色々調べていくと、判明してしまったのです。(いや、して良かった。)
なんと、@validator
デコレータは非推奨、deprecatedだったのです。(@root_validator
も非推奨です。)
その公式ページには、@validator
は非推奨となり、さまざまな新機能と改善を提供する@field_validator
に置き換える必要があるとかなんやら記載されています。なので、@field_validator
でバリデーションの処理を書き換えました。それから、self
を渡していたメソッドにcls
を渡してクラスメソッドにします。
from pydantic import BaseModel, Field, field_validator
class NotionTask(BaseModel):
number: int = -1
con_id: str = Field("default_value", example="TK000012345")
page_id: str = Field("default_value", description="Notion page UUID")
page_icon: str = Field(..., description="Notion page icon")
is_page_archived: bool = Field(False, description="if page is archived")
@field_validator("number")
def validate_number(cls, v):
FIELD_NAME = "con_id"
if v is None:
raise ValueError(f"'{FIELD_NAME}' cannot be None")
if v <= -1:
raise ValueError(f"'{FIELD_NAME}' must be over -1")
return v
@field_validator("con_id")
def validate_con_id(cls, v):
FIELD_NAME = "con_id"
LENGTH_OF_CHARACTERS = 11
if v is None:
raise ValueError(f"'{FIELD_NAME}' cannot be None")
if v == "default_value":
raise ValueError(f"'{FIELD_NAME}' must not be 'default_value'")
if len(v) != LENGTH_OF_CHARACTERS:
raise ValueError(f"'{FIELD_NAME}' must have {LENGTH_OF_CHARACTERS} characters")
return v
@field_validator("page_id")
def validate_page_id(cls, v):
FIELD_NAME = "page_id"
LENGTH_OF_CHARACTERS = 36
if v is None:
raise ValueError(f"'{FIELD_NAME}' cannot be None")
if v == "default_value":
raise ValueError(f"'{FIELD_NAME}' must have and not 'default_value'")
if len(v) != LENGTH_OF_CHARACTERS:
raise ValueError(f"'{FIELD_NAME}' must have {LENGTH_OF_CHARACTERS} characters")
return v
そしてさらに、クラスメソッドに@classmethod
のデコーレーターも付けて実行します。まあ、このコードは大丈夫だった気がします。
from pydantic import BaseModel, Field, field_validator
class NotionTask(BaseModel):
number: int = -1
con_id: str = Field("default_value", example="TK000012345")
page_id: str = Field("default_value", description="Notion page UUID")
page_icon: str = Field(..., description="Notion page icon")
is_page_archived: bool = Field(False, description="if page is archived")
@field_validator("number")
@classmethod
def validate_number(cls, v):
FIELD_NAME = "con_id"
if v is None:
raise ValueError(f"'{FIELD_NAME}' cannot be None")
if v <= -1:
raise ValueError(f"'{FIELD_NAME}' must be over -1")
return v
@field_validator("con_id")
@classmethod
def validate_con_id(cls, v):
FIELD_NAME = "con_id"
LENGTH_OF_CHARACTERS = 11
if v is None:
raise ValueError(f"'{FIELD_NAME}' cannot be None")
if v == "default_value":
raise ValueError(f"'{FIELD_NAME}' must not be 'default_value'")
if len(v) != LENGTH_OF_CHARACTERS:
raise ValueError(f"'{FIELD_NAME}' must have {LENGTH_OF_CHARACTERS} characters")
return v
@field_validator("page_id")
@classmethod
def validate_page_id(cls, v):
FIELD_NAME = "page_id"
LENGTH_OF_CHARACTERS = 36
if v is None:
raise ValueError(f"'{FIELD_NAME}' cannot be None")
if v == "default_value":
raise ValueError(f"'{FIELD_NAME}' must have and not 'default_value'")
if len(v) != LENGTH_OF_CHARACTERS:
raise ValueError(f"'{FIELD_NAME}' must have {LENGTH_OF_CHARACTERS} characters")
return v
しかし・・・、@field_validator
では、先程の@validator
では設定できた、pre=True
とか、always=True
を設定することが出来ません。(pre
とかを設定するとそんな感じのエラーが表示されるんですよね。)なので、フィールドの中に入れる前か入れる後でアノテーションやらバリデーションやらが漏れてしまうんですよね・・・。
他の方法は無いのか。
field_validatorのダブルバリデーションチェック
Pydanticの公式リファレンスのこのページにその方法が掲載されていました。
基本的にはそのページの通りに実装しました。そして実装の方法が2種類あるようです。上記の公式ページでは2種類とも使用して実行順序を示してくれていますが、実際に使う時には片方で事足りると思います。
1つ目は、from pydantic.functional_validators import field_validator
でインポートしたfield_validator
を使用する方法です。まずは、この方法で実装してみます。
元々@field_validator
として実装していたものを、別のValidatorを使ってクロージャ(Callableを返す関数)として作り直すのが煩雑で大変でした。なので、こういう場合は、ゼロから適当なクラスを作って、それをベースに実装し直すと楽かも。
そして、一旦ここまでコードが出来上がりました。これを実行してみます。
from typing import Any, Callable, ClassVar, List, cast
from pydantic import BaseModel, Field, field_validator, ValidationInfo
# Validator makers definition
def make_validator_it_is_not_none(validation_target: str) -> Callable[[Any, ValidationInfo], Any]:
def validator(v: Any, info: ValidationInfo) -> Any:
if v is None:
raise ValueError(f"'{validation_target}' cannot be None")
return v
return validator
def validate_con_id_value(validation_target: str) -> Callable[[Any, ValidationInfo], Any]:
def validator(v: Any, info: ValidationInfo) -> Any:
LENGTH_OF_CHARACTERS = 11
if v == "default_value":
raise ValueError(f"'{validation_target}' must not be 'default_value'")
if len(v) != LENGTH_OF_CHARACTERS:
raise ValueError(f"'{validation_target}' must have {LENGTH_OF_CHARACTERS} characters")
return v
return validator
def make_validator_page_id_value_is_valid(validation_target: str) -> Callable[[Any, ValidationInfo], Any]:
def validator(v: Any, info: ValidationInfo) -> Any:
LENGTH_OF_CHARACTERS = 36
if v == "default_value":
raise ValueError(f"'{validation_target}' must have and not 'default_value'")
if len(v) != LENGTH_OF_CHARACTERS:
raise ValueError(f"'{validation_target}' must have {LENGTH_OF_CHARACTERS} characters")
return v
return validator
# Schema definition
class NotionTask(BaseModel):
number: Annotated[int, -1]
con_id: Annotated[str, Field("default_value", example="TK000012345")]
page_id: Annotated[str, Field("default_value", description="Notion page UUID")]
page_icon: Annotated[str, Field(..., description="Notion page icon")]
is_page_archived: Annotated[bool, Field(False, description="if page is archived")]
val_target = ""
val_target = "number"
val_number_before = field_validator(val_target, mode="before")(
make_validator_it_is_not_none(val_target)
)
val_number_after = field_validator(val_target, mode="after")(
make_validator_number_is_not_negative(val_target)
)
val_target = "con_id"
val_con_id_before = field_validator(val_target, mode="before")(
make_validator_it_is_not_none(val_target)
)
val_con_id_after = field_validator(val_target, mode="after")(
validate_con_id_value(val_target)
)
val_target = "page_id"
val_page_id_before = field_validator(val_target, mode="before")(
make_validator_it_is_not_none(val_target)
)
val_page_id_after = field_validator(val_target, mode="after")(
make_validator_page_id_value_is_valid(val_target)
)
val_target = "is_page_archived"
# Validate nothing
@app.get("/task")
def return_all_tasks():
result = {"number": 1, "con_id": "TK000012345", "page_id": "TK000012345TK000012345TK000012345789", "page_title": "test test title no test", "page_icon": "🍞", "is_page_archived": False}
NotionTask.model_validate(result, context=context)
return [NotionTask(**result)]
エラーメッセージです。
raise PydanticUserError( pydantic.errors.PydanticUserError: A non-annotated attribute was detected: `val_target = ‘con_id’`. All model fields require a type annotation; if `val_target` is not meant to be a field, you may be able to resolve this error by annotating it as a `ClassVar` or updating `model_config[‘ignored_types’]`.
Positive prompt: Anime style, high quality, rough, (from above, upper body), 1 young woman with brown hair, white eyes, Stinking eyes, wearing sports wear, sadness, under the blue sky
negative prompt: violence, sexual, derogatory, toxic
(generated by Imagen 3)
やっぱりシード値が変わると印象変わるなあ。
まあエラーメッセージに色々書いてありますが、結局は、ClassVar
というアノテーションを付けるか、model_config['ignored_types']
を設定するかの2種類の選択肢があるようですね。今回は、ClassVar
を付けてクラス変数として定義するのが良さそうです。(先程も、インスタンスメソッドからクラスメソッドに直しましたね。)
# ↓ インスタンス変数として宣言してしまっている。
val_target = ""
# ↓ クラス変数にする。
from typing import ClassVar
val_target: ClassVar[str] = ""
デフォルト値を設定し忘れると、代入不足が検知できない。
さあ、クラス変数にしたから直ったぞ。と思って実行するのですが・・・、
・・・ここで、例外が発生しない不具合が発生してしまいます。なんてこった・・・。
先程のソースでは、わざとフィールドが不足するようにインスタンスを作成しようとしていました。なので、例外が発生しなければおかしいわけです。しかし発生しなかった・・・。
@app.get("/task")
def return_all_tasks():
result = {"number": 1, "con_id": "TK000012345", "page_id": "TK000012345TK000012345TK000012345789", "page_title": "test test title no test", "page_icon": "🍞", "is_page_archived": False}
NotionTask.model_validate(result, context=context)
return [NotionTask(**result)]
この原因としては2つありました。1つ目は、クラス変数の宣言時に、デフォルト値が設定されていないことが原因でした。そして2つ目は、Field
の中でvalidate_default=True
という設定も必要でした。元々Annotated
の中のField
で設定した"default_value"
は何のフィルターとしても機能していなかったみたいです。
from typing import Any, Callable, ClassVar, List
# ↓ デフォルト値の設定をグローバルインスタンスに一元化した。
class Context(TypedDict):
logs: List[str]
default_value: str
context = Context(logs=[], default_value="default_value")
# ↓ 修正前:デフォルト値が設定されていない。
# Schema definition
class NotionTask(BaseModel):
number: Annotated[int, -1]
con_id: Annotated[str, Field("default_value", example="TK000012345")]
# ↓ 修正後:デフォルト値を設定した。
# Schema definition
class NotionTask(BaseModel):
number: Annotated[int, -1]
con_id: Annotated[str, Field(validate_default=True, example="TK000012345")] = context["default_value"]
修正したコードが下記のものです。これで、1つ目のバリデーション実装である、from pydantic.functional_validators import field_validator
でインポートしたfield_validator
を使用する方法が出来上がりました。(ちなみに、チラホラコメントアウトされている行は、2つ目の方法でバリデーションチェックを実装した部分です。)
from typing import Any, Callable, ClassVar, List, cast
from typing_extensions import Annotated, TypedDict
from pydantic import BaseModel, Field, field_validator, AfterValidator, BeforeValidator, ValidationInfo, HttpUrl
class NotionTask(BaseModel):
number: Annotated[int, -1]
con_id: Annotated[str, Field(validate_default=True, example="TK000012345")] = context["default_value"]
page_id: Annotated[str, Field(validate_default=True, description="Notion page UUID")] = context["default_value"]
page_title: Annotated[str, Field(...)]
status: Annotated[str, Field(validate_default=True, description="Notion page UUID")] = context["default_value"]
page_icon: Annotated[str, Field(..., description="Notion page icon")]
is_page_archived: Annotated[bool, Field(False, description="if page is archived")]
val_target: ClassVar[str] = ""
val_target = "number"
# number: Annotated[int, -1, BeforeValidator(make_validator_it_is_not_none(val_target)), AfterValidator(make_validator_number_is_not_negative(val_target))]
val_number_before = field_validator(val_target, mode="before")(
make_validator_it_is_not_none(val_target)
)
val_number_after = field_validator(val_target, mode="after")(
make_validator_number_is_not_negative(val_target)
)
val_target = "con_id"
# con_id: Annotated[str, Field(validate_default=True, example="TK000012345"), BeforeValidator(make_validator_it_is_not_none(val_target)), AfterValidator(make_validator_values_length_is_equal(val_target, 11))] = context["default_value"]
val_con_id_before = field_validator(val_target, mode="before")(
make_validator_it_is_not_none(val_target)
)
val_con_id_after = field_validator(val_target, mode="after")(
make_validator_values_length_is_equal(val_target, 11)
)
val_target = "page_id"
# page_id: Annotated[str, Field(validate_default=True, description="Notion page UUID"), BeforeValidator(make_validator_it_is_not_none(val_target)), AfterValidator(make_validator_values_length_is_equal(val_target, 36))] = context["default_value"]
val_page_id_before = field_validator(val_target, mode="before")(
make_validator_it_is_not_none(val_target)
)
val_page_id_after = field_validator(val_target, mode="after")(
make_validator_values_length_is_equal(val_target, 36)
)
val_target = "page_title"
# page_title: Annotated[str, Field(validate_default=True), BeforeValidator(make_validator_it_is_not_none(val_target)), AfterValidator(make_validator_value_is_less(val_target, 120))]
val_page_title_before = field_validator(val_target, mode="before")(
make_validator_it_is_not_none(val_target)
)
val_page_title_after = field_validator(val_target, mode="after")(
make_validator_value_is_less(val_target, 120)
)
val_target = "status"
# status: Annotated[str, Field(validate_default=True, description="Notion page UUID"), BeforeValidator(make_validator_it_is_not_none(val_target)), AfterValidator(make_validator_values_length_is_equal(val_target, 36))] = context["default_value"]
val_status_before = field_validator(val_target, mode="before")(
make_validator_it_is_not_none(val_target)
)
val_status_after = field_validator(val_target, mode="after")(
make_validator_values_length_is_equal(val_target, 36)
)
val_target = "page_icon"
# page_icon: Annotated[str, Field(validate_default=True, description="Notion page icon"), BeforeValidator(make_validator_it_is_not_none(val_target)), AfterValidator(make_validator_value_is_less(val_target, 2))]
val_page_icon_before = field_validator(val_target, mode="before")(
make_validator_it_is_not_none(val_target)
)
val_page_icon_after = field_validator(val_target, mode="after")(
make_validator_value_is_less(val_target, 2)
)
val_target = "is_page_archived"
# is_page_archived: Annotated[bool, False, BeforeValidator(make_validator_it_is_not_none(val_target))]
val_is_page_archived_before = field_validator(val_target, mode="before")(
make_validator_it_is_not_none(val_target)
)
# Validates nothing
例えばそのコードをFastAPI上で実行すると、フィールドの代入漏れがあると例外を発生するようになりました。これで、意図しないデータを誤ってレスポンスのボディに載せる挙動を無くすことが出来ます。
from fastapi import FastAPI
app = FastAPI(
title="NotionSynchronizer API",
summary="Retrieves Notion pages and manages with PostgreSQL",
description="You will be able to:\n- Retrieve Notion pages.\n- Store info of Notion pages into PostgreSQL.",
version="0.0.1",
openapi_url="/api/v0/openapi.json",
separate_input_output_schemas=True
)
@app.get("/task")
def return_all_tasks():
result = {"number": 1, "con_id": "TK000012345", "page_id": "TK000012345TK000012345TK000012345789", "page_title": "test test title no test", "page_icon": "🍞", "is_page_archived": False}
NotionTask.model_validate(result, context=context)
return [NotionTask(**result)]
# pydantic_core._pydantic_core.ValidationError: 1 validation error for NotionTask
# status
# Value error, 'status' must not be 'default_value' [type=value_error, input_value='default_value', input_type=str]
Annotatedの中でダブルバリデーションチェック
次に、2つ目の実装方法である、BeforeValidator
とAfterValidator
をAnnotated
の中に入れる実装方法です。
先程のコード内で、from pydantic.functional_validators import field_validator
でインポートしたfield_validator
を使用する実装部分をコメントアウトして、BeforeValidator
とAfterValidator
をAnnotated
の中に入れる方法で実装した部分のコメントアウトを外します。
class NotionTask(BaseModel):
# con_id: Annotated[str, Field(context["default_value"], example="TK000012345")]
# page_id: Annotated[str, Field(context["default_value"], description="Notion page UUID")]
# page_title: Annotated[str, Field(...)]
# status: Annotated[str, Field(context["default_value"], description="Notion page UUID")]
# page_icon: Annotated[str, Field(..., description="Notion page icon")]
# is_page_archived: Annotated[bool, Field(False, description="if page is archived")]
val_target: ClassVar[str] = ""
val_target = "number"
number: Annotated[int, -1, BeforeValidator(make_validator_it_is_not_none(val_target)), AfterValidator(make_validator_number_is_not_negative(val_target))]
# val_number_before = field_validator(val_target, mode="before")(
# make_validator_it_is_not_none(val_target)
# )
# val_number_after = field_validator(val_target, mode="after")(
# make_validator_number_is_not_negative(val_target)
# )
val_target = "con_id"
con_id: Annotated[str, Field(validate_default=True, example="TK000012345"), BeforeValidator(make_validator_it_is_not_none(val_target)), AfterValidator(make_validator_values_length_is_equal(val_target, 11))] = context["default_value"]
# val_con_id_before = field_validator(val_target, mode="before")(
# make_validator_it_is_not_none(val_target)
# )
# val_con_id_after = field_validator(val_target, mode="after")(
# make_validator_values_length_is_equal(val_target, 11)
# )
val_target = "page_id"
page_id: Annotated[str, Field(validate_default=True, description="Notion page UUID"), BeforeValidator(make_validator_it_is_not_none(val_target)), AfterValidator(make_validator_values_length_is_equal(val_target, 36))] = context["default_value"]
# val_page_id_before = field_validator(val_target, mode="before")(
# make_validator_it_is_not_none(val_target)
# )
# val_page_id_after = field_validator(val_target, mode="after")(
# make_validator_values_length_is_equal(val_target, 36)
# )
val_target = "page_title"
page_title: Annotated[str, Field(validate_default=True), BeforeValidator(make_validator_it_is_not_none(val_target)), AfterValidator(make_validator_value_is_less(val_target, 120))]
# val_page_title_before = field_validator(val_target, mode="before")(
# make_validator_it_is_not_none(val_target)
# )
# val_page_title_after = field_validator(val_target, mode="after")(
# make_validator_value_is_less(val_target, 120)
# )
val_target = "status"
status: Annotated[str, Field(validate_default=True, description="Notion page UUID"), BeforeValidator(make_validator_it_is_not_none(val_target)), AfterValidator(make_validator_values_length_is_equal(val_target, 36))] = context["default_value"]
val_status_before = field_validator(val_target, mode="before")(
make_validator_it_is_not_none(val_target)
)
val_status_after = field_validator(val_target, mode="after")(
make_validator_values_length_is_equal(val_target, 36)
)
val_target = "page_icon"
page_icon: Annotated[str, Field(validate_default=True, description="Notion page icon"), BeforeValidator(make_validator_it_is_not_none(val_target)), AfterValidator(make_validator_value_is_less(val_target, 2))]
# val_page_icon_before = field_validator(val_target, mode="before")(
# make_validator_it_is_not_none(val_target)
# )
# val_page_icon_after = field_validator(val_target, mode="after")(
# make_validator_value_is_less(val_target, 2)
# )
val_target = "is_page_archived"
is_page_archived: Annotated[bool, False, BeforeValidator(make_validator_it_is_not_none(val_target))]
# val_is_page_archived_before = field_validator(val_target, mode="before")(
# make_validator_it_is_not_none(val_target)
# )
# Validates nothing
このコードもFastAPI上で実行すると、先程のコードと同様にフィールドの代入漏れがあると例外を発生させるようになりました。(これが代入前のチェック。)そして、フィールドの値自体が文字数の超過や不足が発生している場合も例外が発生するようになりました。(これが代入後のチェック。実は、先程のコードも同様のバリデーションチェックが可能です。)
# statusフィールドが存在しない場合のチェック
@app.get("/task")
def return_all_tasks():
result = {"number": 1, "con_id": "TK000012345", "page_id": "TK000012345TK000012345TK000012345789", "page_title": "test test title no test", "page_icon": "🍞", "is_page_archived": False}
NotionTask.model_validate(result, context=context)
return [NotionTask(**result)]
# pydantic_core._pydantic_core.ValidationError: 1 validation error for NotionTask
# status
# Value error, 'status' must not be 'default_value' [type=value_error, input_value='default_value', input_type=str]
# statusフィールドの文字列が短すぎる場合のチェック
@app.get("/task")
def return_all_tasks():
result = {"number": 1, "con_id": "TK000012345", "page_id": "TK000012345TK000012345TK000012345789", "page_title": "test test title no test", "status": "TK000012345TK000012345TK00001234578", "page_icon": "🍞", "is_page_archived": False}
NotionTask.model_validate(result, context=context)
return [NotionTask(**result)]
# pydantic_core._pydantic_core.ValidationError: 1 validation error for NotionTask
# status
# Value error, 'status' must have 36 characters [type=value_error, input_value='TK000012345TK000012345TK00001234578', input_type=str]
# statusフィールドの文字列が長すぎる場合のチェック
@app.get("/task")
def return_all_tasks():
result = {"number": 1, "con_id": "TK000012345", "page_id": "TK000012345TK000012345TK000012345789", "page_title": "test test title no test", "status": "TK000012345TK000012345TK0000123457890", "page_icon": "🍞", "is_page_archived": False}
NotionTask.model_validate(result, context=context)
return [NotionTask(**result)]
# pydantic_core._pydantic_core.ValidationError: 1 validation error for NotionTask
# status
# Value error, 'status' must have 36 characters [type=value_error, input_value='TK000012345TK000012345TK0000123457890', input_type=str]
このような流れで、Pydanticを用いてオブジェクトのフィールドの型安全を担保したコードを実装することが出来ます。これで自分のコードがより堅牢になると思います。
まとめ
今回の記事は、クラスのフィールドのバリデーションチェックを@validator
で書いてみたが、@validator
はdeprecatedになってて、代わりに@field_validator
を使ってみる。しかし、@field_validator
にはpre
とかalways
フラグが無いから、from pydantic.functional_validators import field_validator
でインポートしたfield_validator
を使用する方法と、BeforeValidator
とAfterValidator
をAnnotate
の中に入れる実装方法で実装してみた。という流れの記事でした。
例えば、データベースにデータを格納する時や、APIとしてレスポンスを返す時などに、Pydanticはより堅牢で明確な機能性を担保してくれるでしょう。
おしまい
よし、形になったぞ・・・
こういうのって最初が一番たいへんですよね。
以上になります!
コメント