경로 파라미터와 숫자 검증

경로 파라미터와 숫자 검증 (Path Parameters and Numeric Validations)

Query로 쿼리 파라미터에 대해 더 많은 검증과 메타데이터를 선언할 수 있는 것과 마찬가지로, Path로 경로 파라미터에 같은 종류의 검증과 메타데이터를 선언할 수 있습니다.

출처: 공식문서

Path 임포트하기 (Import Path)

먼저 fastapi에서 Path를, 그리고 Annotated를 임포트하세요:

from typing import Annotated

from fastapi import FastAPI, Path, Query

app = FastAPI()

@app.get("/items/{item_id}")
async def read_items(
    item_id: Annotated[int, Path(title="The ID of the item to get")],
    q: Annotated[str | None, Query(alias="item-query")] = None,
):
    results = {"item_id": item_id}
    if q:
        results.update({"q": q})
    return results

🤓 다른 버전과 변형

!!! tip "팁" 가능하다면 Annotated 버전을 사용하는 걸 권장합니다.

from fastapi import FastAPI, Path, Query

app = FastAPI()

@app.get("/items/{item_id}")
async def read_items(
    item_id: int = Path(title="The ID of the item to get"),
    q: str | None = Query(default=None, alias="item-query"),
):
    results = {"item_id": item_id}
    if q:
        results.update({"q": q})
    return results

!!! note "참고" FastAPI는 버전 0.95.0에서 Annotated를 지원하기 시작했고(그리고 권장하기 시작했어요).

더 오래된 버전을 사용하면 `Annotated`를 쓰려다 오류가 발생할 수 있습니다.

`Annotated`를 사용하기 전에 [FastAPI 버전 업그레이드](https://fastapi.tiangolo.com/deployment/versions/#upgrading-the-fastapi-versions)를 적어도 0.95.1 이상으로 하세요.

메타데이터 선언하기 (Declare metadata)

Query와 같은 모든 파라미터를 선언할 수 있습니다.

예를 들어 경로 파라미터 item_idtitle 메타데이터 값을 선언하려면 이렇게 입력하면 됩니다:

from typing import Annotated

from fastapi import FastAPI, Path, Query

app = FastAPI()

@app.get("/items/{item_id}")
async def read_items(
    item_id: Annotated[int, Path(title="The ID of the item to get")],
    q: Annotated[str | None, Query(alias="item-query")] = None,
):
    results = {"item_id": item_id}
    if q:
        results.update({"q": q})
    return results

🤓 다른 버전과 변형

!!! tip "팁" 가능하다면 Annotated 버전을 사용하는 걸 권장합니다.

from fastapi import FastAPI, Path, Query

app = FastAPI()

@app.get("/items/{item_id}")
async def read_items(
    item_id: int = Path(title="The ID of the item to get"),
    q: str | None = Query(default=None, alias="item-query"),
):
    results = {"item_id": item_id}
    if q:
        results.update({"q": q})
    return results

!!! note "참고" 경로 파라미터는 경로의 일부여야 하기 때문에 항상 필수입니다. None으로 선언하거나 기본값을 설정해도 아무 영향이 없으며, 항상 필수로 남습니다.

필요한 순서대로 파라미터 배치하기 (Order the parameters as you need)

!!! tip "팁" Annotated를 사용한다면 이 내용은 아마 그리 중요하거나 필요하지 않을 거예요.

쿼리 파라미터 q를 필수 str로 선언하고 싶다고 합시다.

그 파라미터에 대해 다른 것은 선언할 필요가 없으니, Query를 쓸 필요가 없습니다.

하지만 경로 파라미터 item_id에는 여전히 Path를 써야 합니다. 그리고 어떤 이유로든 Annotated를 쓰고 싶지 않다고 가정해 봅시다.

Python은 "기본값"이 있는 값을 "기본값"이 없는 값보다 앞에 두면 불평합니다.

하지만 순서를 바꿔서 기본값이 없는 값(쿼리 파라미터 q)을 앞에 둘 수 있습니다.

FastAPI에게는 상관없습니다. 이름, 타입, 그리고 기본 선언(Query, Path 등)으로 파라미터를 감지하므로 순서를 신경 쓰지 않아요.

그래서 함수를 이렇게 선언할 수 있습니다:

from fastapi import FastAPI, Path

app = FastAPI()

@app.get("/items/{item_id}")
async def read_items(q: str, item_id: int = Path(title="The ID of the item to get")):
    results = {"item_id": item_id}
    if q:
        results.update({"q": q})
    return results

🤓 다른 버전과 변형

from typing import Annotated

from fastapi import FastAPI, Path

app = FastAPI()

@app.get("/items/{item_id}")
async def read_items(
    q: str, item_id: Annotated[int, Path(title="The ID of the item to get")]
):
    results = {"item_id": item_id}
    if q:
        results.update({"q": q})
    return results

하지만 Annotated를 사용하면 이 문제가 없다는 걸 명심하세요. Query()Path()에 함수 파라미터 기본값을 사용하지 않으니까 순서가 상관없어요.

from typing import Annotated

from fastapi import FastAPI, Path

app = FastAPI()

@app.get("/items/{item_id}")
async def read_items(
    q: str, item_id: Annotated[int, Path(title="The ID of the item to get")]
):
    results = {"item_id": item_id}
    if q:
        results.update({"q": q})
    return results

🤓 다른 버전과 변형

!!! tip "팁" 가능하다면 Annotated 버전을 사용하는 걸 권장합니다.

from fastapi import FastAPI, Path

app = FastAPI()

@app.get("/items/{item_id}")
async def read_items(q: str, item_id: int = Path(title="The ID of the item to get")):
    results = {"item_id": item_id}
    if q:
        results.update({"q": q})
    return results

파라미터 배치하기, 트릭 (Order the parameters as you need, tricks)

!!! tip "팁" Annotated를 사용한다면 이 내용은 아마 그리 중요하거나 필요하지 않을 거예요.

여기 유용할 수 있는 작은 트릭이 있는데, 자주 필요하진 않을 거예요.

만약 다음을 원한다면:

  • q 쿼리 파라미터를 Query도, 기본값도 없이 선언
  • 경로 파라미터 item_idPath로 선언
  • 서로 다른 순서로 두기
  • Annotated를 쓰지 않기

...Python에는 이에 딱 맞는 특별한 문법이 있습니다.

함수의 첫 번째 파라미터로 *를 넘기세요.

Python은 그 *로 아무것도 하지 않지만, 이후의 모든 파라미터는 키워드 인자(키-값 쌍)로 호출되어야 한다는 것(kwargs라고도 알려진)을 알게 됩니다. 기본값이 없더라도요.

from fastapi import FastAPI, Path

app = FastAPI()

@app.get("/items/{item_id}")
async def read_items(*, item_id: int = Path(title="The ID of the item to get"), q: str):
    results = {"item_id": item_id}
    if q:
        results.update({"q": q})
    return results

🤓 다른 버전과 변형

from typing import Annotated

from fastapi import FastAPI, Path

app = FastAPI()

@app.get("/items/{item_id}")
async def read_items(
    item_id: Annotated[int, Path(title="The ID of the item to get")], q: str
):
    results = {"item_id": item_id}
    if q:
        results.update({"q": q})
    return results

Annotated로 더 나은 방법 (Better with Annotated)

Annotated를 사용하면 함수 파라미터 기본값을 쓰지 않으므로 이런 문제가 없고, 아마 *도 쓸 필요가 없을 거라는 점을 명심하세요.

from typing import Annotated

from fastapi import FastAPI, Path

app = FastAPI()

@app.get("/items/{item_id}")
async def read_items(
    item_id: Annotated[int, Path(title="The ID of the item to get")], q: str
):
    results = {"item_id": item_id}
    if q:
        results.update({"q": q})
    return results

🤓 다른 버전과 변형

!!! tip "팁" 가능하다면 Annotated 버전을 사용하는 걸 권장합니다.

from fastapi import FastAPI, Path

app = FastAPI()

@app.get("/items/{item_id}")
async def read_items(*, item_id: int = Path(title="The ID of the item to get"), q: str):
    results = {"item_id": item_id}
    if q:
        results.update({"q": q})
    return results

숫자 검증: 크거나 같음 (greater than or equal)

QueryPath(그리고 나중에 보게 될 다른 것들)로 숫자 제약 조건을 선언할 수 있습니다.

여기서 ge=1을 쓰면, item_id1보다 "크거나 같은(greater than or equal)" 정수여야 합니다.

from typing import Annotated

from fastapi import FastAPI, Path

app = FastAPI()

@app.get("/items/{item_id}")
async def read_items(
    item_id: Annotated[int, Path(title="The ID of the item to get", ge=1)], q: str
):
    results = {"item_id": item_id}
    if q:
        results.update({"q": q})
    return results

🤓 다른 버전과 변형

!!! tip "팁" 가능하다면 Annotated 버전을 사용하는 걸 권장합니다.

from fastapi import FastAPI, Path

app = FastAPI()

@app.get("/items/{item_id}")
async def read_items(
    *, item_id: int = Path(title="The ID of the item to get", ge=1), q: str
):
    results = {"item_id": item_id}
    if q:
        results.update({"q": q})
    return results

숫자 검증: 크거나 같음과 작거나 같음 (greater than and less than or equal)

다음에도 동일하게 적용됩니다:

  • gt: greater than (보다 큼)
  • le: less than or equal (작거나 같음)
from typing import Annotated

from fastapi import FastAPI, Path

app = FastAPI()

@app.get("/items/{item_id}")
async def read_items(
    item_id: Annotated[int, Path(title="The ID of the item to get", gt=0, le=1000)],
    q: str,
):
    results = {"item_id": item_id}
    if q:
        results.update({"q": q})
    return results

🤓 다른 버전과 변형

!!! tip "팁" 가능하다면 Annotated 버전을 사용하는 걸 권장합니다.

from fastapi import FastAPI, Path

app = FastAPI()

@app.get("/items/{item_id}")
async def read_items(
    *,
    item_id: int = Path(title="The ID of the item to get", gt=0, le=1000),
    q: str,
):
    results = {"item_id": item_id}
    if q:
        results.update({"q": q})
    return results

숫자 검증: float, 크고 작음 (greater than and less than)

숫자 검증은 float 값에도 동작합니다.

ge가 아니라 gt를 선언할 수 있다는 게 여기서 중요해집니다. 예를 들어 값이 0보다는 크되 1보다 작아야 한다는 식으로 요구할 수 있으니까요.

그래서 0.5는 유효한 값입니다. 하지만 0.0이나 0은 유효하지 않아요.

lt도 마찬가지입니다.

from typing import Annotated

from fastapi import FastAPI, Path, Query

app = FastAPI()

@app.get("/items/{item_id}")
async def read_items(
    *,
    item_id: Annotated[int, Path(title="The ID of the item to get", ge=0, le=1000)],
    q: str,
    size: Annotated[float, Query(gt=0, lt=10.5)],
):
    results = {"item_id": item_id}
    if q:
        results.update({"q": q})
    if size:
        results.update({"size": size})
    return results

🤓 다른 버전과 변형

!!! tip "팁" 가능하다면 Annotated 버전을 사용하는 걸 권장합니다.

from fastapi import FastAPI, Path, Query

app = FastAPI()

@app.get("/items/{item_id}")
async def read_items(
    *,
    item_id: int = Path(title="The ID of the item to get", ge=0, le=1000),
    q: str,
    size: float = Query(gt=0, lt=10.5),
):
    results = {"item_id": item_id}
    if q:
        results.update({"q": q})
    if size:
        results.update({"size": size})
    return results

요약 (Recap)

Query, Path(그리고 아직 보지 못한 다른 것들)로, Query Parameters and String Validations와 같은 방식으로 메타데이터와 문자열 검증을 선언할 수 있습니다.

또한 숫자 검증도 선언할 수 있습니다:

  • gt: greater than (보다 큼)
  • ge: greater than or equal (크거나 같음)
  • lt: less than (보다 작음)
  • le: less than or equal (작거나 같음)

!!! note "참고" Query, Path, 그리고 나중에 보게 될 다른 클래스들은 공통 Param 클래스의 하위 클래스입니다.

모두 지금까지 본 추가 검증과 메타데이터를 위한 같은 파라미터들을 공유합니다.

!!! info "기술적 세부사항 (Technical Details)" fastapi에서 Query, Path 등을 임포트하면, 그것들은 사실 함수입니다.

호출하면 같은 이름의 클래스 인스턴스를 반환하죠.

그래서 `Query`라는 함수를 임포트하고, 호출하면 같은 이름인 `Query`라는 클래스의 인스턴스를 반환합니다.

클래스를 직접 쓰는 대신 이런 함수들이 있는 이유는, 편집기가 그 타입에 대한 오류를 표시하지 않도록 하기 위해서입니다.

그렇게 하면 그 오류들을 무시하도록 커스텀 설정을 추가하지 않고도 평범한 편집기와 코딩 도구를 쓸 수 있어요.

더 알아보기 (Learn more)

  • 경로 파라미터는 항상 필수이며, Path로 메타데이터(title, alias 등)와 숫자 검증(gt, ge, lt, le)을 선언할 수 있습니다.
  • Annotated를 쓰면 함수 파라미터의 기본값 순서 문제를 피할 수 있고, * 트릭도 필요 없습니다.
  • Query, Path, Body, Header 등은 공통 Param 클래스에서 파생되며, 같은 검증/메타데이터 파라미터를 공유합니다.