Zum Inhalt

Separate OpenAPI-Schemas für Eingabe und Ausgabe oder nicht

Bei Verwendung von Pydantic v2 ist die generierte OpenAPI etwas genauer und korrekter als zuvor. 😎

Tatsächlich gibt es in einigen Fällen sogar zwei JSON-Schemas in OpenAPI für dasselbe Pydantic-Modell für Eingabe und Ausgabe, je nachdem, ob sie Defaultwerte haben.

Sehen wir uns an, wie das funktioniert und wie Sie es bei Bedarf ändern können.

Pydantic-Modelle für Eingabe und Ausgabe

Nehmen wir an, Sie haben ein Pydantic-Modell mit Defaultwerten wie dieses:

from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: str | None = None

# Code unterhalb weggelassen 👇

👀 Vollständige Dateivorschau

from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: str | None = None


app = FastAPI()


@app.post("/items/")
def create_item(item: Item):
    return item


@app.get("/items/")
def read_items() -> list[Item]:
    return [
        Item(
            name="Portal Gun",
            description="Device to travel through the multi-rick-verse",
        ),
        Item(name="Plumbus"),
    ]
from typing import Optional

from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: Optional[str] = None

# Code unterhalb weggelassen 👇

👀 Vollständige Dateivorschau

from typing import Optional

from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: Optional[str] = None


app = FastAPI()


@app.post("/items/")
def create_item(item: Item):
    return item


@app.get("/items/")
def read_items() -> list[Item]:
    return [
        Item(
            name="Portal Gun",
            description="Device to travel through the multi-rick-verse",
        ),
        Item(name="Plumbus"),
    ]
from typing import List, Union

from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: Union[str, None] = None

# Code unterhalb weggelassen 👇

👀 Vollständige Dateivorschau

from typing import List, Union

from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: Union[str, None] = None


app = FastAPI()


@app.post("/items/")
def create_item(item: Item):
    return item


@app.get("/items/")
def read_items() -> List[Item]:
    return [
        Item(
            name="Portal Gun",
            description="Device to travel through the multi-rick-verse",
        ),
        Item(name="Plumbus"),
    ]

Modell für Eingabe

Wenn Sie dieses Modell wie hier als Eingabe verwenden:

from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: str | None = None


app = FastAPI()


@app.post("/items/")
def create_item(item: Item):
    return item

# Code unterhalb weggelassen 👇

👀 Vollständige Dateivorschau

from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: str | None = None


app = FastAPI()


@app.post("/items/")
def create_item(item: Item):
    return item


@app.get("/items/")
def read_items() -> list[Item]:
    return [
        Item(
            name="Portal Gun",
            description="Device to travel through the multi-rick-verse",
        ),
        Item(name="Plumbus"),
    ]
from typing import Optional

from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: Optional[str] = None


app = FastAPI()


@app.post("/items/")
def create_item(item: Item):
    return item

# Code unterhalb weggelassen 👇

👀 Vollständige Dateivorschau

from typing import Optional

from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: Optional[str] = None


app = FastAPI()


@app.post("/items/")
def create_item(item: Item):
    return item


@app.get("/items/")
def read_items() -> list[Item]:
    return [
        Item(
            name="Portal Gun",
            description="Device to travel through the multi-rick-verse",
        ),
        Item(name="Plumbus"),
    ]
from typing import List, Union

from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: Union[str, None] = None


app = FastAPI()


@app.post("/items/")
def create_item(item: Item):
    return item

# Code unterhalb weggelassen 👇

👀 Vollständige Dateivorschau

from typing import List, Union

from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: Union[str, None] = None


app = FastAPI()


@app.post("/items/")
def create_item(item: Item):
    return item


@app.get("/items/")
def read_items() -> List[Item]:
    return [
        Item(
            name="Portal Gun",
            description="Device to travel through the multi-rick-verse",
        ),
        Item(name="Plumbus"),
    ]

... dann ist das Feld description nicht erforderlich. Weil es den Defaultwert None hat.

Eingabemodell in der Dokumentation

Sie können überprüfen, dass das Feld description in der Dokumentation kein rotes Sternchen enthält, es ist nicht als erforderlich markiert:

Modell für die Ausgabe

Wenn Sie jedoch dasselbe Modell als Ausgabe verwenden, wie hier:

from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: str | None = None


app = FastAPI()


@app.post("/items/")
def create_item(item: Item):
    return item


@app.get("/items/")
def read_items() -> list[Item]:
    return [
        Item(
            name="Portal Gun",
            description="Device to travel through the multi-rick-verse",
        ),
        Item(name="Plumbus"),
    ]
from typing import Optional

from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: Optional[str] = None


app = FastAPI()


@app.post("/items/")
def create_item(item: Item):
    return item


@app.get("/items/")
def read_items() -> list[Item]:
    return [
        Item(
            name="Portal Gun",
            description="Device to travel through the multi-rick-verse",
        ),
        Item(name="Plumbus"),
    ]
from typing import List, Union

from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: Union[str, None] = None


app = FastAPI()


@app.post("/items/")
def create_item(item: Item):
    return item


@app.get("/items/")
def read_items() -> List[Item]:
    return [
        Item(
            name="Portal Gun",
            description="Device to travel through the multi-rick-verse",
        ),
        Item(name="Plumbus"),
    ]

... dann, weil description einen Defaultwert hat, wird es, wenn Sie für dieses Feld nichts zurückgeben, immer noch diesen Defaultwert haben.

Modell für Ausgabe-Responsedaten

Wenn Sie mit der Dokumentation interagieren und die Response überprüfen, enthält die JSON-Response den Defaultwert (null), obwohl der Code nichts in eines der description-Felder geschrieben hat:

Das bedeutet, dass es immer einen Wert hat, der Wert kann jedoch manchmal None sein (oder null in JSON).

Das bedeutet, dass Clients, die Ihre API verwenden, nicht prüfen müssen, ob der Wert vorhanden ist oder nicht. Sie können davon ausgehen, dass das Feld immer vorhanden ist. In einigen Fällen hat es jedoch nur den Defaultwert None.

Um dies in OpenAPI zu kennzeichnen, markieren Sie dieses Feld als erforderlich, da es immer vorhanden sein wird.

Aus diesem Grund kann das JSON-Schema für ein Modell unterschiedlich sein, je nachdem, ob es für Eingabe oder Ausgabe verwendet wird:

  • für die Eingabe ist description nicht erforderlich
  • für die Ausgabe ist es erforderlich (und möglicherweise None oder, in JSON-Begriffen, null)

Ausgabemodell in der Dokumentation

Sie können das Ausgabemodell auch in der Dokumentation überprüfen. Sowohl name als auch description sind mit einem roten Sternchen als erforderlich markiert:

Eingabe- und Ausgabemodell in der Dokumentation

Und wenn Sie alle verfügbaren Schemas (JSON-Schemas) in OpenAPI überprüfen, werden Sie feststellen, dass es zwei gibt, ein Item-Input und ein Item-Output.

Für Item-Input ist description nicht erforderlich, es hat kein rotes Sternchen.

Aber für Item-Output ist description erforderlich, es hat ein rotes Sternchen.

Mit dieser Funktion von Pydantic v2 ist Ihre API-Dokumentation präziser, und wenn Sie über automatisch generierte Clients und SDKs verfügen, sind diese auch präziser, mit einer besseren Entwicklererfahrung und Konsistenz. 🎉

Schemas nicht trennen

Nun gibt es einige Fälle, in denen Sie möglicherweise dasselbe Schema für Eingabe und Ausgabe haben möchten.

Der Hauptanwendungsfall hierfür besteht wahrscheinlich darin, dass Sie das mal tun möchten, wenn Sie bereits über einige automatisch generierte Client-Codes/SDKs verfügen und im Moment nicht alle automatisch generierten Client-Codes/SDKs aktualisieren möchten, möglicherweise später, aber nicht jetzt.

In diesem Fall können Sie diese Funktion in FastAPI mit dem Parameter separate_input_output_schemas=False deaktivieren.

Info

Unterstützung für separate_input_output_schemas wurde in FastAPI 0.102.0 hinzugefügt. 🤓

from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: str | None = None


app = FastAPI(separate_input_output_schemas=False)


@app.post("/items/")
def create_item(item: Item):
    return item


@app.get("/items/")
def read_items() -> list[Item]:
    return [
        Item(
            name="Portal Gun",
            description="Device to travel through the multi-rick-verse",
        ),
        Item(name="Plumbus"),
    ]
from typing import Optional

from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: Optional[str] = None


app = FastAPI(separate_input_output_schemas=False)


@app.post("/items/")
def create_item(item: Item):
    return item


@app.get("/items/")
def read_items() -> list[Item]:
    return [
        Item(
            name="Portal Gun",
            description="Device to travel through the multi-rick-verse",
        ),
        Item(name="Plumbus"),
    ]
from typing import List, Union

from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: Union[str, None] = None


app = FastAPI(separate_input_output_schemas=False)


@app.post("/items/")
def create_item(item: Item):
    return item


@app.get("/items/")
def read_items() -> List[Item]:
    return [
        Item(
            name="Portal Gun",
            description="Device to travel through the multi-rick-verse",
        ),
        Item(name="Plumbus"),
    ]

Gleiches Schema für Eingabe- und Ausgabemodelle in der Dokumentation

Und jetzt wird es ein einziges Schema für die Eingabe und Ausgabe des Modells geben, nur Item, und es wird description als nicht erforderlich kennzeichnen:

Dies ist das gleiche Verhalten wie in Pydantic v1. 🤓