一刻も早くpydantic v2 + fastapiでopenapi-generatorを使うために

## この記事に書いてあること
- 今すぐpydantic v2 + fastapi v100を使うため、アドホックにOAS3.0.xなopenapi.jsonを吐けるようにする話
## この記事に書いてないこと
- pydantic v2の変更点
- JSON SchemaとOASの細かいバージョン差異


## 前置き
ついにpydantic v2がリリースされました!
パフォーマンスの向上も勿論ですが、それ以外にも[requiredとnullableの関係が整理](https://docs.pydantic.dev/latest/blog/pydantic-v2/#required-vs-nullable-cleanup)されたり, [namespaceが整理](https://docs.pydantic.dev/latest/blog/pydantic-v2/#model-namespace-cleanup)されたりと、便利な変更が盛り沢山です。
[fastapiも早速v2対応をしたv0.100をリリース](https://github.com/tiangolo/fastapi/releases/tag/0.100.0)しているので、さて移行するぞ!と、行きたいところなのですがここで問題があります。
[JSON SchemaとOpenAPIの差分に関する問題](https://spec.openapis.org/oas/v3.0.3#schema-object)です。

ざっくりOASとJSON Schemaの差分について整理すると
- おそらく一番メジャーなopenapi code generatorである[openapi-generator](https://github.com/OpenAPITools/openapi-generator)はOAS3.0までしか対応していない(generatorの種類も多いし[OAS3.1対応はまだかかりそう…](https://github.com/OpenAPITools/openapi-generator/issues/9083))
- OAS3.0.xはJSON Schema Draft 5(JSON Schema Specification Wright Draft 00)のサブセットを使っている
- [pydantic v1ではJSON Schema Draft 7を採用](https://github.com/pydantic/pydantic/issues/4666#issuecomment-1332147554)していたが、[pydantic v2ではJSON Schema Draft 2020-12をターゲットにしている](https://github.com/pydantic/pydantic/issues/4666#issuecomment-1298456812)
- [OAS3.1はJSON Schema Draft 2020-12準拠になる](https://spec.openapis.org/oas/v3.1.0#schema-object)

ということで、要はpydantic v2の吐くschemaはOAS3.1でないと読めないという問題が発生します。


## 本題
手元で触っていて困ったのは
1. Literalがv1ではstringのenumだったが、v2ではconstになる
2. T | Noneがv1ではtype: T + nullable: trueだったが、v2ではanyof[{ type: T }, { type: null }]になる

の2点です

1は
```python
class A(BaseModel):
    a: Literal['a']
```
がv1では
```JSON
{"title": "A", "type": "object", "properties": {"a": {"title": "A", "enum": ["a"], "type": "string"}}, "required": ["a"]}
```
からv2では
```JSON
{"properties": {"a": {"const": "a", "title": "A"}}, "required": ["a"], "title": "A", "type": "object"}
```
になるという問題です

2は
```python
class B(BaseModel):
    b: int | None
```
がv1では
```JSON
{"title": "B", "type": "object", "properties": {"b": {"title": "B", "type": "integer"}}}
```
からv2では
```JSON
{"properties": {"b": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "B"}}, "required": ["b"], "title": "B", "type": "object"}
```
になります。

## アドホックな解決策
ということで上記スキーマをv2の方からv1の方に変換するために`FastAPI.openapi`を無理矢理上書きします。
```python
from typing import Any, Callable

from fastapi import FastAPI
from fastapi.openapi.utils import get_openapi


def _override_schema(schema: dict[str, Any]) -> dict[str, Any]:
    new_schema = schema
    if next((x for x in schema.get("anyOf", []) if x.get("type", None) == "null"), None) and len(schema.get("anyOf", [])) == 2:
        new_schema = {
            **next((x for x in schema["anyOf"] if x.get("type", None) != "null")),
            "nullable": True,
        }
        if schema.get("title", None):
            new_schema["title"] = schema["title"]
        schema = new_schema
    elif schema.get("const", None) is not None:
        new_schema = {
            "type": "string",
            "enum": [schema["const"]],
        }
        if schema.get("title", None):
            new_schema["title"] = schema["title"]
        schema = new_schema
    else:
        schema = new_schema
    return schema


def _override_openapi_schema(openapi_schema: dict[str, Any]) -> dict[str, Any]:
    for key in openapi_schema["components"]["schemas"]:
        openapi_schema["components"]["schemas"][key]["properties"] = {
            key: _override_schema(value) for key, value in openapi_schema["components"]["schemas"][key]["properties"].items()
        }
    for path in openapi_schema["paths"]:
        for method in openapi_schema["paths"][path]:
            if openapi_schema["paths"][path][method].get("parameters", None):
                openapi_schema["paths"][path][method]["parameters"] = [
                    {
                        **parameter,
                        "schema": _override_schema(parameter["schema"]),
                    } for parameter in openapi_schema["paths"][path][method]["parameters"]
                ]
    return openapi_schema


app = FastAPI(title="app_name", version="app_version")


def gen_openapi() -> dict[str, Any]:
    if app.openapi_schema:
        return app.openapi_schema
    openapi_schema = get_openapi(
        title="app_name",
        version="app_version",
        routes=app.routes,
        openapi_version="3.0.2",
    )
    app.openapi_schema = _override_openapi_schema(openapi_schema)
    return app.openapi_schema

app.openapi = gen_openapi
```

というような感じで、上記2つの問題を無理矢理変換することでOASv3.0まで対応のcode generatorでもpydantic v2 + fastapi v0.100を使うことができるようになります。

手元で詰まったのはこの2点のみでしたが、仕様上は他にも変換するべき項目があると思うので(OASが対応していないkeywordは[この辺参照](https://swagger.io/docs/specification/data-models/keywords/))、見つけたら是非教えてください。