입력·출력용 OpenAPI 스키마 분리 여부

입력·출력용 OpenAPI 스키마 분리 여부 (Separate OpenAPI Schemas for Input and Output or Not)

Pydantic v2가 출시된 이후로, 생성되는 OpenAPI는 예전보다 조금 더 정확하고 올바르게 됐어요. 😎

사실 어떤 경우에는 OpenAPI 안에 같은 Pydantic 모델에 대한 JSON Schema가 두 개 생길 수도 있어요. 입력용과 출력용이죠. **기본값(default values)**이 있는지에 따라 달라요.

이게 어떻게 동작하는지, 그리고 필요하다면 어떻게 바꾸는지 볼게요.

출처: 공식문서

입력과 출력을 위한 Pydantic 모델

기본값이 있는 Pydantic 모델이 있다고 해 볼게요. 이렇게요:

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 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"),
    ]

...그러면 description 필드는 필수가 아니게 돼요. 기본값이 None이기 때문이에요.

문서에서의 입력 모델

문서에서 확인해 보면 description 필드에 **빨간 별표(red asterisk)**가 없어요. 필수로 표시되지 않죠.

출력용 모델

하지만 같은 모델을 여기처럼 출력으로 쓴다면:

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"),
    ]

...description에 기본값이 있기 때문에, 그 필드에 아무것도 반환하지 않아도 여전히 그 기본값을 갖게 돼요.

출력 응답 데이터용 모델

문서와 상호작용해서 응답을 확인해 보면, 코드가 description 필드 중 하나에 아무것도 추가하지 않았는데도 JSON 응답에 기본값(null)이 들어 있어요.

항상 값이 있다는 뜻이에요. 값이 None(JSON에선 null)일 수 있을 뿐이죠.

그 말은 여러분 API를 쓰는 클라이언트가 값이 있는지 없는지 확인할 필요가 없다는 거예요. 그 필드가 항상 거기 있을 거라고 가정할 수 있지만, 어떤 경우에는 기본값인 None을 가질 뿐이에요.

OpenAPI에서 이걸 설명하는 방법은 그 필드를 **required(필수)**로 표시하는 거예요. 항상 있으니까요.

그래서 모델의 JSON Schema는 입력으로 쓰이느냐 출력으로 쓰이느냐에 따라 달라질 수 있어요:

  • 입력에서는 description필수가 아님
  • 출력에서는 필수(그리고 None, 즉 JSON 용어로 null일 수 있음)

문서에서의 출력 모델

문서에서 출력 모델도 확인할 수 있어요. namedescription 둘 다 빨간 별표required로 표시돼요.

문서에서의 입력·출력 모델

그리고 OpenAPI에서 쓸 수 있는 모든 Schema(JSON Schemas)를 확인하면 두 개가 있다는 걸 볼 수 있어요. 하나는 Item-Input, 하나는 Item-Output이에요.

Item-Input에서 description필수가 아니에요. 빨간 별표가 없죠.

하지만 Item-Output에서 description필수예요. 빨간 별표가 있어요.

Pydantic v2의 이 기능 덕분에 여러분의 API 문서가 더 정밀해지고, 자동 생성된 클라이언트와 SDK가 있다면 그것들도 더 정밀해져서 더 나은 개발자 경험과 일관성을 얻게 돼요. 🎉

스키마 분리하지 않기

이제 입력과 출력에 같은 스키마를 쓰고 싶은 경우도 있어요.

아마 가장 큰 사용 사례는 이미 자동 생성된 클라이언트 코드/SDK가 있고, 아직 모든 자동 생성 코드/SDK를 업데이트하고 싶지 않을 때예요. 언젠가는 하고 싶겠지만, 지금 당장은 아닐 수 있죠.

그런 경우 FastAPI에서 separate_input_output_schemas=False 파라미터로 이 기능을 끌 수 있어요.

참고separate_input_output_schemas 지원은 FastAPI 0.102.0에서 추가됐어요. 🤓

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"),
    ]

문서에서의 입력·출력 모델용 단일 스키마

이제 그 모델의 입력과 출력에 대해 단 하나의 스키마만 존재하고, Item 하나뿐이며, description필수가 아님으로 표시돼요.

더 알아보기 (Learn more)