🕺

[pydantic V2] model_configという名前のフィールドを作るには?

2023/07/30に公開

背景

pydantic V1では、BaseModelを継承したモデルの各種設定のために Config という名前の内部クラスを定義する仕様でした。これがV2では、model_config = pydantic.ConfigDict(...) の要領でフィールドとして定義するように改められています(V1の方法もまだ使えますが非推奨)。
https://docs.pydantic.dev/2.1/usage/model_config/

class MyModel(BaseModel):
    name: str
    model_config = ConfigDict(str_to_upper=True)
model = MyModel(name="Alice")
print(model.model_dump())  # {'name': 'ALICE'}

さてここで疑問です。model_config という名前をフィールド名に使いたいときはどうすれば?
稀かもしれませんが、変え難いスキーマとしてどうしてもこの名前が使いたいと仮定します。

検証環境

pydantic==2.1.1
fastapi==0.100.1

先に答え

alias
https://docs.pydantic.dev/latest/usage/fields/#field-aliases

class MyModel(BaseModel):
    name: str
    my_model_config: str = Field(alias="model_config")

dumpするときはby_alias=Trueを指定します。model_dump_jsonでも同様です。aliasした場合、初期化ではその別名の方で指定します。逆にdictからモデルを作る際は何も指定しなくてよいようです。

# モデル -> dict
model = MyModel(name="Alice", model_config="XXX")
print(model.model_dump(by_alias=True)))  #  {'name': 'ALICE', 'model_config': 'XXX' }

# dict -> モデル
d = {"name": "Alice", "model_config": "AAA"}
print(MyModel.model_validate(d))  # name='ALICE' my_model_config='AAA'

本件はpydanticの一種の予約語と被った場合でした。同様にPythonの予約語等(class, def等)と被る場合にもaliasは有効でしょう。

FastAPIでaliasを使う場合

pydanticと言えばFastAPIですが、by_alias=Trueのように自分で指定しての変換は普通書きません。しかし心配無用で、自動的にaliasのほうを使ってくれるようです。

from fastapi import FastAPI
from pydantic import BaseModel, Field, ConfigDict


class MyModel(BaseModel):
    name: str
    my_model_config: str = Field(alias="model_config")
    model_config = ConfigDict(str_to_upper=True)


app = FastAPI()


@app.post("/post")
def post(m: MyModel) -> MyModel:
    return MyModel(name=m.name+"-san", model_config=m.my_model_config+"BBB")
> curl -X POST -H "Content-Type:application/json" -d "{""name"":""Bob"",""model_config"":""AAA""}" http://127.0.0.1:8000/post

{"name":"BOB-SAN","model_config":"AAABBB"}

ちなみに、逆にaliasを使いたくない場合はこちらが参考になります。
https://stackoverflow.com/questions/69673518/return-pydantic-model-with-field-names-instead-of-alias-as-fastapi-response

失敗の履歴

上の解に至るまでに色々試した結果を残しておきます。

pydanticの仕様など無視して普通に定義

スルーされます。

class MyModel(BaseModel):
    name: str
    model_config: str 


model = MyModel(name="Alice", model_config="aaa")
print(model1.model_dump())

結果からはmodel_configは消えました。

{'name': 'Alice'}

model_configの中身を以下のように見てみると、結局dict(TypedDict)になっており、指定が効いていない(誤った型付けしただけ?)になっています。そもそもこの方法が仮に妥当でも、pydantic仕様の方のmodel_configを併せて使いたい場合に困りますので、やはりナシです。

print(model.model_config)  # {}
print(type(model.model_config))  # <class 'dict'>

V1流にConfig内部クラス

やはりスルーされます。この場合はConfigの設定が勝ちます。

class MyModel(BaseModel):
    name: str
    model_config: str

    class Config:
        str_to_upper = True


model = MyModel(name="Alice", model_config="aaa")
print(model.model_dump())
print(model.model_config)
print(type(model.model_config))

Config内部クラスに定義しても model_config フィールドに値が入るのですね。

{'name': 'Alice'}
{'str_to_upper': True}
<class 'dict'>

脱線: ConfigDictをmodel_config以外に使ってみる

本題とそれますが、以下のように model_config というフィールド名以外でConfigDictを使ってみると、盛大に怒られます。typoした結果こうなるケースがあり得そうですが、気づけるということで安心ですね。

class MyModel(BaseModel):
    name: str
    my_config: ConfigDict = ConfigDict(str_to_upper=True)
エラー出力の例
Traceback (most recent call last):
  File "C:\Projects\Python\pydantic2_sample\main.py", line 5, in <module>
    class MyModel(BaseModel):
  File "C:\Projects\Python\pydantic2_sample\.venv\Lib\site-packages\pydantic\_internal\_model_construction.py", line 173, in __new__
    complete_model_class(
  File "C:\Projects\Python\pydantic2_sample\.venv\Lib\site-packages\pydantic\_internal\_model_construction.py", line 463, in complete_model_class
    schema = cls.__get_pydantic_core_schema__(cls, handler)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Projects\Python\pydantic2_sample\.venv\Lib\site-packages\pydantic\main.py", line 549, in __get_pydantic_core_schema__
    return __handler(__source)
           ^^^^^^^^^^^^^^^^^^^
  File "C:\Projects\Python\pydantic2_sample\.venv\Lib\site-packages\pydantic\_internal\_schema_generation_shared.py", line 82, in __call__
    schema = self._handler(__source_type)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Projects\Python\pydantic2_sample\.venv\Lib\site-packages\pydantic\_internal\_generate_schema.py", line 426, in generate_schema
    return self._generate_schema_for_type(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Projects\Python\pydantic2_sample\.venv\Lib\site-packages\pydantic\_internal\_generate_schema.py", line 458, in _generate_schema_for_type
    schema = self._generate_schema(obj)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Projects\Python\pydantic2_sample\.venv\Lib\site-packages\pydantic\_internal\_generate_schema.py", line 684, in _generate_schema
    return self._model_schema(obj)
           ^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Projects\Python\pydantic2_sample\.venv\Lib\site-packages\pydantic\_internal\_generate_schema.py", line 528, in _model_schema
    {k: self._generate_md_field_schema(k, v, decorators) for k, v in fields.items()},
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Projects\Python\pydantic2_sample\.venv\Lib\site-packages\pydantic\_internal\_generate_schema.py", line 528, in <dictcomp>
    {k: self._generate_md_field_schema(k, v, decorators) for k, v in fields.items()},
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Projects\Python\pydantic2_sample\.venv\Lib\site-packages\pydantic\_internal\_generate_schema.py", line 848, in _generate_md_field_schema
    common_field = self._common_field_schema(name, field_info, decorators)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Projects\Python\pydantic2_sample\.venv\Lib\site-packages\pydantic\_internal\_generate_schema.py", line 900, in _common_field_schema
    schema = self._apply_annotations(
             ^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Projects\Python\pydantic2_sample\.venv\Lib\site-packages\pydantic\_internal\_generate_schema.py", line 1586, in _apply_annotations
    schema = get_inner_schema(source_type)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Projects\Python\pydantic2_sample\.venv\Lib\site-packages\pydantic\_internal\_schema_generation_shared.py", line 82, in __call__
    schema = self._handler(__source_type)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Projects\Python\pydantic2_sample\.venv\Lib\site-packages\pydantic\_internal\_generate_schema.py", line 1550, in inner_handler
    schema = self._generate_schema(obj)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Projects\Python\pydantic2_sample\.venv\Lib\site-packages\pydantic\_internal\_generate_schema.py", line 689, in _generate_schema
    return self.match_type(obj)
           ^^^^^^^^^^^^^^^^^^^^
  File "C:\Projects\Python\pydantic2_sample\.venv\Lib\site-packages\pydantic\_internal\_generate_schema.py", line 737, in match_type
    return self._typed_dict_schema(obj, None)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Projects\Python\pydantic2_sample\.venv\Lib\site-packages\pydantic\_internal\_generate_schema.py", line 1066, in _typed_dict_schema
    for field_name, annotation in get_type_hints_infer_globalns(
                                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Projects\Python\pydantic2_sample\.venv\Lib\site-packages\pydantic\_internal\_fields.py", line 50, in get_type_hints_infer_globalns
    return get_type_hints(obj, globalns=globalns, localns=localns, include_extras=include_extras)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Program Files\Python311\Lib\typing.py", line 2302, in get_type_hints
    value = _eval_type(value, base_globals, base_locals)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Program Files\Python311\Lib\typing.py", line 359, in _eval_type
    return t._evaluate(globalns, localns, recursive_guard)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Program Files\Python311\Lib\typing.py", line 854, in _evaluate
    eval(self.__forward_code__, globalns, localns),
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<string>", line 1, in <module>
NameError: name '_GenerateSchema' is not defined

Discussion