PydanticV2へのアップデート。苦労したことなど
概要
pydanticV1 → V2にアップデートする作業を行ったので、自分が道中調べたところなどをメモしていきます。
どなたかの参考になれば幸いです。
作業内容
BaseModelインスタンスで利用する関数名の変更
シンプルな変更です。
公式のMigration Guideにあるので粛々やってしまいます。
(あくまで非推奨なので元のままでも動きますが、やることはっきりしていて時間かからないので、やったほうが良いと思います。)
公式からの抜粋
エディタで検索して置換していきます。(目視なしで全部やると他も引っかかりそうなので。。。もっと良い方法ご存知の方コメントいただけると嬉しいです。)
str型への甘えの修正(そもそもやっちゃダメ!)
オニオンアーキテクチャを組んでいて、フロントエンドへはNoneとかをわかりやすい表現に変換したいので文字列として送りたいけど、バックエンドでは数値で扱っているデータとかやってしまっていると、痛い目合います。(アップグレード関係なく、そもそもやめましょう。)
V1ではstr型にfloatを入れてもエラーが出ませんでしたが、V2ではしっかり出ます。
上記のような時は、ちゃんとdomainモデルとフロント用の出力モデルを分けて、関数でパースするようにしておきましょう。
class DomainSampleModel:
value:float
class FrontResponseSampleModel:
value:str
def from_entity(self, entity:DomainSampleModel):
self.value = str(entity.value)
return self
BaseConfig → ConfigDict
いくつか廃止、変更されたものがあります。
基本的には公式の「Changes to config」に記載の通りです。
ただ、個人的には、これに関しては以下のapi仕様書の方がわかりやすかったです。
V1からの変更も書いてくれています。
allow_mutation → frozenのdefalutがTrueからFalseに変わったのできをつけましょう。
自分がわかりにくかったのが、copy_on_model_validationの扱いです。
以下URLのように、validateの時のコピー動作がそもそもなくなり、オプション指定が必要なくなったようです。(背景実はよくわかっていない。)
validator → field_validator
自分が使っていた範疇なら、ほぼ名前の変更だけでなんとかなりました。
each_itemsが不要になった(複数指定するときは自動で捌いてくれる)くらいでした。
公式のMigration Guideの「Changes to validators」に書いてありますね。
V1ではよくcls, valueを使っていましたが、ValidationInfoをこの際使っていくようにしました。
(V1でも使えたかわかっていません。。。)
公式のありがたいサンプルコードを以下に記載
from pydantic import BaseModel, ValidationInfo, field_validator
class Model(BaseModel):
text: str
@field_validator('text')
@classmethod
def remove_stopwords(cls, v: str, info: ValidationInfo):
context = info.context
if context:
stopwords = context.get('stopwords', set())
v = ' '.join(w for w in v.split() if w.lower() not in stopwords)
return v
data = {'text': 'This is an example document'}
print(Model.model_validate(data)) # no context
#> text='This is an example document'
print(Model.model_validate(data, context={'stopwords': ['this', 'is', 'an']}))
#> text='example document'
print(Model.model_validate(data, context={'stopwords': ['document']}))
#> text='This is an example'
root_validator → model_validator
結構変更多いです。
概念から変わっている印象を自分は受けたので、しっかりmodel_validatorの動きを勉強して臨みましょう。
既存のコードを文法変更するというよりは、公式の説明を見て再設計したほうが早いかもしれません。
自分はこのサンプルコードが一番イメージ動作しやすかったです。
公式のありがたいサンプルコードを以下に記載
from typing import Any
from pydantic import BaseModel, ValidationError, model_validator
class UserModel(BaseModel):
username: str
password1: str
password2: str
@model_validator(mode='before')
@classmethod
def check_card_number_omitted(cls, data: Any) -> Any:
if isinstance(data, dict):
assert (
'card_number' not in data
), 'card_number should not be included'
return data
@model_validator(mode='after')
def check_passwords_match(self) -> 'UserModel':
pw1 = self.password1
pw2 = self.password2
if pw1 is not None and pw2 is not None and pw1 != pw2:
raise ValueError('passwords do not match')
return self
print(UserModel(username='scolvin', password1='zxcvbn', password2='zxcvbn'))
#> username='scolvin' password1='zxcvbn' password2='zxcvbn'
try:
UserModel(username='scolvin', password1='zxcvbn', password2='zxcvbn2')
except ValidationError as e:
print(e)
"""
1 validation error for UserModel
Value error, passwords do not match [type=value_error, input_value={'username': 'scolvin', '... 'password2': 'zxcvbn2'}, input_type=dict]
"""
try:
UserModel(
username='scolvin',
password1='zxcvbn',
password2='zxcvbn',
card_number='1234',
)
except ValidationError as e:
print(e)
"""
1 validation error for UserModel
Assertion failed, card_number should not be included
assert 'card_number' not in {'card_number': '1234', 'password1': 'zxcvbn', 'password2': 'zxcvbn', 'username': 'scolvin'} [type=assertion_error, input_value={'username': 'scolvin', '..., 'card_number': '1234'}, input_type=dict]
感想
大きなアプリケーションだと動作確認が大変ですが、自分の場合は基本BaseModelに頼っているので、その周りを注目すれば良かったです。
オニオンアーキテクチャをしっかり設計できていると(model部分を分離できていると)、こういったバージョンアップがかなりしやすいことを体感しました。これからも気をつけたいと思います。
また、今までなんとなくで理解していたコードを見直す良い勉強の機会になりました。
V1, V2両方の操作を想像して、施したであろう改良を想像、体感するのも楽しかったです。
素晴らしいエンジニアたちがせっかく良いものとしてバージョンアップしてくれているので、今後もできるだけついていきたいですね。
Discussion