Pydantic v2 モデルに v1 モデルのフィールドをつくる

## 課題

基本的に v1/v2 のモデルを混合して使うことはできないのですが、Pydantic v1 → v2 のマイグレーションの都合上どうしても v1/v2 を共存させる必要がありました。

例えば以下のように v2 モデルの中に v1 モデルのフィールドを作るとエラーが発生します。

```python
from pydantic import BaseModel
from pydantic.v1 import BaseModel as BaseModelV1


class Child(BaseModelV1):
    name: str


class Parent(BaseModel):
    child: Child


if __name__ == "__main__":
    print("Parent schema  :", Parent.model_json_schema())
    print("Parent instance:", parent := Parent.model_validate({"child": {"name": "John"}}))
    print("Parent dumped  :", parent.model_dump())
```

```
...
    raise PydanticInvalidForJsonSchema(f'Cannot generate a JsonSchema for {error_info}')
pydantic.errors.PydanticInvalidForJsonSchema: Cannot generate a JsonSchema for core_schema.PlainValidatorFunctionSchema ({'type': 'with-info', 'function': <bound method BaseModel.validate of <class '__main__.Child'>>})

For further information visit https://errors.pydantic.dev/2.10/u/invalid-for-json-schema
```

v1 と v2 では validation/serialization の挙動が大きく変わっていることもあり、公式にはこれらを併用する手段が提供されていません。

## 解決策

Pydantic v2 ではモデルに `__get_pydantic_core_schema__`/`__get_pydantic_json_schema__` メソッドを実装することで独自のスキーマ情報を渡すことができます[^custom-schema]。
今回は Pydantic v1 のモデルにこれらのメソッドを追加して併用可能にすることにします。

例えば以下のような関数を実装します。

```python
from typing import Any, TypeVar

from pydantic import BaseModel, GetCoreSchemaHandler, GetJsonSchemaHandler
from pydantic.json_schema import JsonSchemaValue
from pydantic.v1 import BaseModel as BaseModelV1
from pydantic_core import core_schema

_TV1 = TypeVar("_TV1", bound=BaseModelV1 | type[BaseModelV1])


def _get_pydantic_core_schema(
    cls: type[BaseModelV1],
    source_type: Any,
    handler: GetCoreSchemaHandler,
) -> core_schema.CoreSchema:
    del source_type, handler
    # NOTE: ちゃんと実装すれば v1 モデルから適切な CoreSchema を生成できそうな気がしますが、
    # 非常に複雑になるためひとまず　AnySchema  を返しています。
    return core_schema.no_info_before_validator_function(
        function=lambda value: value if isinstance(value, cls) else cls.parse_obj(value),
        schema=core_schema.any_schema(),
    )


def _get_pydantic_json_schema(
    cls: type[BaseModelV1],
    source_type: Any,
    handler: GetJsonSchemaHandler,
) -> JsonSchemaValue:
    del source_type, handler
    return cls.schema()


def pydantic_v2_compatible(model: _TV1) -> _TV1:
    given = model
    cls = given.__class__ if isinstance(given, BaseModel) else given
    setattr(cls, "__get_pydantic_core_schema__", classmethod(_get_pydantic_core_schema))
    setattr(cls, "__get_pydantic_json_schema__", classmethod(_get_pydantic_json_schema))
    return given

...

@pydantic_v2_compatible
class Child(BaseModelV1):
    name: str
```

これを使って先述のコードを再度実行します。

```
Parent schema  : {'properties': {'child': {'properties': {'name': {'title': 'Name', 'type': 'string'}}, 'required': ['name'], 'title': 'Child', 'type': 'object'}}, 'required': ['child'], 'title': 'Parent', 'type': 'object'}
Parent instance: child=Child(name='John')
Parent dumped  : {'child': Child(name='John')}
```

JSON Schema の生成と validation はうまく動作しています。

~~`model_dump` の結果を辞書型にする場合は serializer を設定する必要があります[^custom-serializer]。~~ (→ 追記)

```python
def serialize_model(model: BaseModelV1, info: core_schema.SerializationInfo) -> dict[str, Any]:
    return model.dict(
        include=info.include,
        exclude=info.exclude,
        by_alias=info.by_alias,
        exclude_defaults=info.exclude_defaults,
        exclude_none=info.exclude_none,
        exclude_unset=info.exclude_unset,
    )

class Parent(BaseModel):
    child: Annotated[Child, PlainSerializer(serialize_model)]
```

```
Parent schema  : {'properties': {'child': {'properties': {'name': {'title': 'Name', 'type': 'string'}, 'age': {'title': 'Age', 'type': 'integer'}}, 'required': ['name'], 'title': 'Child', 'type': 'object'}}, 'required': ['child'], 'title': 'Parent', 'type': 'object'}
Parent instance: child=Child(name='John')
Parent dumped  : {'child': {'name': 'John'}}
```

## おわりに

用途によっては他にも変更が必要かもしれませんが、これでひとまず基本的なユースケースでは問題なく動作するようになりました。

[^custom-schema]: https://docs.pydantic.dev/latest/concepts/types/#customizing-validation-with-__get_pydantic_core_schema__
[^custom-serializer]: https://docs.pydantic.dev/latest/concepts/serialization/#custom-serializers


## 追記 (2025-06-07)

`CoreSchema` を設定する際に `serialization` 引数を通して serialization の挙動を設定できます。

```python
    return core_schema.no_info_after_validator_function(
        lambda v: v if isinstance(v, cls) else cls.parse_obj(v),
        core_schema.any_schema(
            serialization=core_schema.plain_serializer_function_ser_schema(
                function=_serialize_model,
                info_arg=True,
            )
        ),
    )
```