LoginSignup
1
0

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

Posted at

この記事に書いてあること

  • 今すぐpydantic v2 + fastapi v100を使うため、アドホックにOAS3.0.xなopenapi.jsonを吐けるようにする話

この記事に書いてないこと

  • pydantic v2の変更点
  • JSON SchemaとOASの細かいバージョン差異

前置き

ついにpydantic v2がリリースされました!
パフォーマンスの向上も勿論ですが、それ以外にもrequiredとnullableの関係が整理されたり, namespaceが整理されたりと、便利な変更が盛り沢山です。
fastapiも早速v2対応をしたv0.100をリリースしているので、さて移行するぞ!と、行きたいところなのですがここで問題があります。
JSON SchemaとOpenAPIの差分に関する問題です。

ざっくりOASとJSON Schemaの差分について整理すると

ということで、要は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は

class A(BaseModel):
    a: Literal['a']

がv1では

{"title": "A", "type": "object", "properties": {"a": {"title": "A", "enum": ["a"], "type": "string"}}, "required": ["a"]}

からv2では

{"properties": {"a": {"const": "a", "title": "A"}}, "required": ["a"], "title": "A", "type": "object"}

になるという問題です

2は

class B(BaseModel):
    b: int | None

がv1では

{"title": "B", "type": "object", "properties": {"b": {"title": "B", "type": "integer"}}}

からv2では

{"properties": {"b": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "B"}}, "required": ["b"], "title": "B", "type": "object"}

になります。

アドホックな解決策

ということで上記スキーマをv2の方からv1の方に変換するためにFastAPI.openapiを無理矢理上書きします。

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はこの辺参照)、見つけたら是非教えてください。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0