PydanticのJsonはそのまま使わないほうがいい
始めに
Pydantic
にはJson
という便利な型があります。便利ではあるのですが、素のJson
型では開発しづらい点があったので自分のアプリケーションでは拡張して使っています。
今回の記事では何に困ったのか、どのように拡張したのかを記載します。
環境
- Python
- 3.12.4
- FastAPI
- 0.114.2
- Pydantic
- 2.9.1
ユースケース
- フロントで自由なデータ構造を定義してバックエンドとしては単純に保存する。また、フロントで取り出せるようにバックエンドで保存したデータ構造をフロントに返却する。
困ったケース
Pydantic
のJson
で定義するとフロントで定義した自由なデータ構造がJson
型であることを保証してくれます。ですので、フロントからバックエンドへの接続時には問題ありません。問題は、バックエンドからフロントにデータ構造を返却した時でした。PythonとしてはJson
とはList
かDict
にあたります。
# 次はすべてJson
{}
[1, 2, 3]
{'value': {'nestedKey': 'value'}}
そのため、フロントから得たデータ型をそのまま返却した時に次のエラーが出力されてしまいます。
pydantic_core._pydantic_core.ValidationError: 2 validation errors for _Test3
value.json[any]
JSON input should be string, bytes or bytearray [type=json_type, input_value={'value': [{'a': 'b'}]}, input_type=dict]
For further information visit https://errors.pydantic.dev/2.8/v/json_type
value.is-instance[JSON]
Input should be an instance of JSON [type=is_instance_of, input_value={'value': [{'a': 'b'}]}, input_type=dict]
For further information visit https://errors.pydantic.dev/2.8/v/is_instance_of
一応、毎回json.dumps
で文字列に変換すれば問題はないのですが、影響範囲がかなり大きい状態になっていたので型で解決することにします。
実装
Pydantic
のJson
だけでなく、Dict
とList
も定義しました。このように定義することで、フロントからバックエンドはデータを受け取りつつ、バックエンドからフロントに容易に返却できます。
JsonField = Annotated[
Json[Any] | Dict[str, Any] | List[Any],
Field(examples=['{"value": {"a": "b"}}', ["A", "B"], {'value': [{'a': 'b'}]}]),
]
一応、Json
を残しておくメリットもないのですが、エラーメッセージはJson
のものを使用したいので、そのままにしています。
pydantic_core._pydantic_core.ValidationError: 3 validation errors for _Test2
value.json[any]
Invalid JSON: key must be a string at line 1 column 2 [type=json_invalid, input_value='{[1, 2, 3]}', input_type=str]
For further information visit https://errors.pydantic.dev/2.8/v/json_invalid
value.dict[str,any]
Input should be a valid dictionary [type=dict_type, input_value='{[1, 2, 3]}', input_type=str]
For further information visit https://errors.pydantic.dev/2.8/v/dict_type
value.list[any]
Input should be a valid list [type=list_type, input_value='{[1, 2, 3]}', input_type=str]
For further information visit https://errors.pydantic.dev/2.8/v/list_type
実装およびテストコードについては次のとおりです。
import json
import pytest
from typing import Annotated, Dict, Any, List
from pydantic import BaseModel, Json, Field
JsonField = Annotated[
Json[Any] | Dict[str, Any] | List[Any],
Field(examples=['{"value": {"a": "b"}}', ["A", "B"], {'value': [{'a': 'b'}]}]),
]
class TestJson:
class _Test2(BaseModel):
value: JsonField
@pytest.mark.parametrize(
"value",
[
'{"value": {"a": "b"}}',
["A, B"],
{'value': [{'a': 'b'}]}
],
)
async def test_02(self, value: Any):
# Dict と Listを定義しているので、json.dumpsは不要
instance = self._Test2(value=value)
self._Test2.model_validate(instance.model_dump())
別のメリット
また、型定義をしつつField
のexamples
を伝えることによって、OpenAPIのサンプルレスポンスがわかりやすくなります。内部的にはJSON
であればなんでも受け取れるのですが、サンプルがないと事前にシリアライズした状態でリクエストしてほしいように見えます。
10001.png
10002.png
各項目でexamples
を定義するのは面倒ですので、Annotated
で定義箇所を1つにまとめられるのはメリットです。
ソースコード
終わりに
Json
型をそのまま使うことはないかもしれませんが、覚えておくと少し便利です。
参考情報
- PydanticのJson型
- JSONの定義(RFC 8259)
Discussion