콘텐츠로 이동

쿼리 매개변수와 문자열 검증

FastAPI를 사용하면 매개변수에 대한 추가 정보 및 검증을 선언할 수 있습니다.

이 응용 프로그램을 예로 들어보겠습니다:

from fastapi import FastAPI

app = FastAPI()


@app.get("/items/")
async def read_items(q: str | None = None):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results
🤓 Other versions and variants
from typing import Union

from fastapi import FastAPI

app = FastAPI()


@app.get("/items/")
async def read_items(q: Union[str, None] = None):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results

쿼리 매개변수 qstr | None 자료형입니다. 즉, str 자료형이지만 None 역시 될 수 있음을 뜻하고, 실제로 기본값은 None이기 때문에 FastAPI는 이 매개변수가 필수가 아니라는 것을 압니다.

참고

FastAPI는 q의 기본값이 = None이기 때문에 필수가 아님을 압니다.

str | None을 사용하면 편집기가 더 나은 지원과 오류 탐지를 제공하게 해줍니다.

추가 검증

q가 선택적이지만 값이 주어질 때마다 길이가 50자를 초과하지 않게 강제하려 합니다.

QueryAnnotated 임포트

이를 위해 먼저 다음을 임포트합니다:

  • fastapi에서 Query
  • typing에서 Annotated
from typing import Annotated

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(q: Annotated[str | None, Query(max_length=50)] = None):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results
🤓 Other versions and variants
from typing import Annotated, Union

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(q: Annotated[Union[str, None], Query(max_length=50)] = None):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results

Tip

Prefer to use the Annotated version if possible.

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(q: str | None = Query(default=None, max_length=50)):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results

Tip

Prefer to use the Annotated version if possible.

from typing import Union

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(q: Union[str, None] = Query(default=None, max_length=50)):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results

정보

FastAPI는 0.95.0 버전에서 Annotated 지원을 추가했고(그리고 이를 권장하기 시작했습니다).

이전 버전을 사용하면 Annotated를 사용하려고 할 때 오류가 발생합니다.

Annotated를 사용하기 전에 최소 0.95.1 버전으로 FastAPI 버전 업그레이드를 진행하세요.

q 매개변수의 타입에 Annotated 사용하기

이전에 Python Types Intro에서 Annotated를 사용해 매개변수에 메타데이터를 추가할 수 있다고 말씀드린 것을 기억하시나요?

이제 FastAPI에서 사용할 차례입니다. 🚀

다음과 같은 타입 어노테이션이 있었습니다:

q: str | None = None
q: Union[str, None] = None

여기서 Annotated로 감싸서 다음과 같이 만듭니다:

q: Annotated[str | None] = None
q: Annotated[Union[str, None]] = None

두 버전 모두 같은 의미로, qstr 또는 None이 될 수 있는 매개변수이며 기본값은 None입니다.

이제 재미있는 부분으로 넘어가 봅시다. 🎉

q 매개변수의 AnnotatedQuery 추가하기

이제 이 Annotated에 더 많은 정보를 넣을 수 있으므로(이 경우에는 추가 검증), Annotated 안에 Query를 추가하고 max_length 매개변수를 50으로 설정합니다:

from typing import Annotated

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(q: Annotated[str | None, Query(max_length=50)] = None):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results
🤓 Other versions and variants
from typing import Annotated, Union

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(q: Annotated[Union[str, None], Query(max_length=50)] = None):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results

Tip

Prefer to use the Annotated version if possible.

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(q: str | None = Query(default=None, max_length=50)):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results

Tip

Prefer to use the Annotated version if possible.

from typing import Union

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(q: Union[str, None] = Query(default=None, max_length=50)):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results

기본값은 여전히 None이므로, 매개변수는 여전히 선택적입니다.

하지만 Annotated 안에 Query(max_length=50)를 넣음으로써, 이 값에 대해 추가 검증을 적용하고 최대 50자까지만 허용하도록 FastAPI에 알려줍니다. 😎

여기서는 쿼리 매개변수이기 때문에 Query()를 사용합니다. 나중에 Path(), Body(), Header(), Cookie()와 같이 Query()와 동일한 인자를 받는 것들도 보게 될 것입니다.

이제 FastAPI는 다음을 수행합니다:

  • 최대 길이가 50자인지 확인하도록 데이터를 검증합니다
  • 데이터가 유효하지 않을 때 클라이언트에게 명확한 오류를 보여줍니다
  • OpenAPI 스키마 경로 처리에 매개변수를 문서화합니다(따라서 자동 문서 UI에 표시됩니다)

대안(이전 방식): 기본값으로 Query 사용

이전 FastAPI 버전(0.95.0 이전)에서는 Annotated에 넣는 대신, 매개변수의 기본값으로 Query를 사용해야 했습니다. 주변에서 이 방식을 사용하는 코드를 볼 가능성이 높기 때문에 설명해 드리겠습니다.

새 코드를 작성할 때와 가능할 때는 위에서 설명한 대로 Annotated를 사용하세요. 여러 장점이 있고(아래에서 설명합니다) 단점은 없습니다. 🍰

다음은 함수 매개변수의 기본값으로 Query()를 사용하면서 max_length를 50으로 설정하는 방법입니다:

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(q: str | None = Query(default=None, max_length=50)):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results
🤓 Other versions and variants
from typing import Annotated

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(q: Annotated[str | None, Query(max_length=50)] = None):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results
from typing import Annotated, Union

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(q: Annotated[Union[str, None], Query(max_length=50)] = None):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results

Tip

Prefer to use the Annotated version if possible.

from typing import Union

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(q: Union[str, None] = Query(default=None, max_length=50)):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results

이 경우(Annotated를 사용하지 않는 경우) 함수에서 기본값 NoneQuery()로 바꿔야 하므로, 이제 Query(default=None)로 기본값을 설정해야 합니다. (최소한 FastAPI 입장에서는) 이 인자는 해당 기본값을 정의하는 것과 같은 목적을 수행합니다.

그러므로:

q: str | None = Query(default=None)

...위 코드는 기본값이 None인 선택적 매개변수를 만들며, 아래와 동일합니다:

q: str | None = None

하지만 Query 버전은 이것이 쿼리 매개변수임을 명시적으로 선언합니다.

그 다음, Query로 더 많은 매개변수를 전달할 수 있습니다. 지금의 경우 문자열에 적용되는 max_length 매개변수입니다:

q: str | None = Query(default=None, max_length=50)

이는 데이터를 검증할 것이고, 데이터가 유효하지 않다면 명백한 오류를 보여주며, OpenAPI 스키마 경로 처리에 매개변수를 문서화 합니다.

기본값으로 Query 사용 또는 Annotated에 넣기

Annotated 안에서 Query를 사용할 때는 Querydefault 매개변수를 사용할 수 없다는 점을 기억하세요.

대신 함수 매개변수의 실제 기본값을 사용하세요. 그렇지 않으면 일관성이 깨집니다.

예를 들어, 다음은 허용되지 않습니다:

q: Annotated[str, Query(default="rick")] = "morty"

...왜냐하면 기본값이 "rick"인지 "morty"인지 명확하지 않기 때문입니다.

따라서 (가능하면) 다음과 같이 사용합니다:

q: Annotated[str, Query()] = "rick"

...또는 오래된 코드베이스에서는 다음과 같은 코드를 찾게 될 것입니다:

q: str = Query(default="rick")

Annotated의 장점

함수 매개변수의 기본값 방식 대신 Annotated를 사용하는 것을 권장합니다. 여러 이유로 더 좋기 때문입니다. 🤓

함수 매개변수기본값실제 기본값이 되므로, 전반적으로 Python에 더 직관적입니다. 😌

FastAPI 없이도 다른 곳에서 같은 함수를 호출할 수 있고, 예상대로 동작합니다. 필수 매개변수(기본값이 없는 경우)가 있다면 편집기가 오류로 알려줄 것이고, 필수 매개변수를 전달하지 않고 실행하면 Python도 오류를 냅니다.

Annotated를 사용하지 않고 (이전) 기본값 스타일을 사용하면, FastAPI 없이 다른 곳에서 함수를 호출할 때도 제대로 동작하도록 함수에 인자를 전달해야 한다는 것을 기억해야 합니다. 그렇지 않으면 값이 기대와 다르게 됩니다(예: str 대신 QueryInfo 같은 것). 그리고 편집기도 경고하지 않고 Python도 그 함수를 실행할 때는 불평하지 않으며, 오직 내부 동작에서 오류가 발생할 때만 문제가 드러납니다.

Annotated는 하나 이상의 메타데이터 어노테이션을 가질 수 있기 때문에, 이제 Typer 같은 다른 도구에서도 같은 함수를 사용할 수 있습니다. 🚀

검증 더 추가하기

min_length 매개변수도 추가할 수 있습니다:

from typing import Annotated

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(
    q: Annotated[str | None, Query(min_length=3, max_length=50)] = None,
):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results
🤓 Other versions and variants
from typing import Annotated, Union

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(
    q: Annotated[Union[str, None], Query(min_length=3, max_length=50)] = None,
):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results

Tip

Prefer to use the Annotated version if possible.

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(q: str | None = Query(default=None, min_length=3, max_length=50)):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results

Tip

Prefer to use the Annotated version if possible.

from typing import Union

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(
    q: Union[str, None] = Query(default=None, min_length=3, max_length=50),
):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results

정규식 추가

매개변수와 일치해야 하는 정규 표현식 pattern을 정의할 수 있습니다:

from typing import Annotated

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(
    q: Annotated[
        str | None, Query(min_length=3, max_length=50, pattern="^fixedquery$")
    ] = None,
):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results
🤓 Other versions and variants
from typing import Annotated, Union

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(
    q: Annotated[
        Union[str, None], Query(min_length=3, max_length=50, pattern="^fixedquery$")
    ] = None,
):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results

Tip

Prefer to use the Annotated version if possible.

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(
    q: str | None = Query(
        default=None, min_length=3, max_length=50, pattern="^fixedquery$"
    ),
):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results

Tip

Prefer to use the Annotated version if possible.

from typing import Union

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(
    q: Union[str, None] = Query(
        default=None, min_length=3, max_length=50, pattern="^fixedquery$"
    ),
):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results

이 특정 정규표현식 패턴은 전달 받은 매개변수 값이 다음을 만족하는지 검사합니다:

  • ^: 뒤따르는 문자로 시작하며, 앞에는 문자가 없습니다.
  • fixedquery: 정확히 fixedquery 값을 가집니다.
  • $: 여기서 끝나며, fixedquery 이후로 더 이상 문자가 없습니다.

"정규 표현식" 개념에 대해 상실감을 느꼈다면 걱정하지 않아도 됩니다. 많은 사람에게 어려운 주제입니다. 아직은 정규 표현식 없이도 많은 작업들을 할 수 있습니다.

이제 필요할 때 언제든지 FastAPI에서 직접 사용할 수 있다는 사실을 알게 되었습니다.

기본값

물론 None이 아닌 다른 기본값을 사용할 수도 있습니다.

q 쿼리 매개변수에 min_length3으로 설정하고, 기본값을 "fixedquery"로 선언하고 싶다고 해봅시다:

from typing import Annotated

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(q: Annotated[str, Query(min_length=3)] = "fixedquery"):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results
🤓 Other versions and variants

Tip

Prefer to use the Annotated version if possible.

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(q: str = Query(default="fixedquery", min_length=3)):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results

참고

None을 포함해 어떤 타입이든 기본값을 가지면 매개변수는 선택적(필수 아님)이 됩니다.

필수 매개변수

더 많은 검증이나 메타데이터를 선언할 필요가 없는 경우, 다음과 같이 기본값을 선언하지 않고 쿼리 매개변수 q를 필수로 만들 수 있습니다:

q: str

아래 대신:

q: str | None = None

하지만 이제는 예를 들어 다음과 같이 Query로 선언합니다:

q: Annotated[str | None, Query(min_length=3)] = None

따라서 Query를 사용하면서 값을 필수로 선언해야 할 때는, 기본값을 선언하지 않으면 됩니다:

from typing import Annotated

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(q: Annotated[str, Query(min_length=3)]):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results
🤓 Other versions and variants

Tip

Prefer to use the Annotated version if possible.

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(q: str = Query(min_length=3)):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results

필수지만 None 가능

매개변수가 None을 허용하지만 여전히 필수라고 선언할 수 있습니다. 이렇게 하면 값이 None이더라도 클라이언트는 값을 반드시 전송해야 합니다.

이를 위해 None이 유효한 타입이라고 선언하되, 기본값은 선언하지 않으면 됩니다:

from typing import Annotated

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(q: Annotated[str | None, Query(min_length=3)]):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results
🤓 Other versions and variants
from typing import Annotated, Union

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(q: Annotated[Union[str, None], Query(min_length=3)]):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results

Tip

Prefer to use the Annotated version if possible.

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(q: str | None = Query(min_length=3)):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results

Tip

Prefer to use the Annotated version if possible.

from typing import Union

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(q: Union[str, None] = Query(min_length=3)):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results

쿼리 매개변수 리스트 / 다중값

Query로 쿼리 매개변수를 명시적으로 정의할 때 값들의 리스트를 받도록 선언할 수도 있고, 다른 말로 하면 여러 값을 받도록 선언할 수도 있습니다.

예를 들어, URL에서 여러 번 나타날 수 있는 q 쿼리 매개변수를 선언하려면 다음과 같이 작성할 수 있습니다:

from typing import Annotated

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(q: Annotated[list[str] | None, Query()] = None):
    query_items = {"q": q}
    return query_items
🤓 Other versions and variants
from typing import Annotated, Union

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(q: Annotated[Union[list[str], None], Query()] = None):
    query_items = {"q": q}
    return query_items

Tip

Prefer to use the Annotated version if possible.

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(q: list[str] | None = Query(default=None)):
    query_items = {"q": q}
    return query_items

Tip

Prefer to use the Annotated version if possible.

from typing import Union

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(q: Union[list[str], None] = Query(default=None)):
    query_items = {"q": q}
    return query_items

그 다음, 아래와 같은 URL로:

http://localhost:8000/items/?q=foo&q=bar

여러 q 쿼리 매개변수 값들(foobar)을 파이썬 list경로 처리 함수함수 매개변수 q에서 받게 됩니다.

따라서 해당 URL에 대한 응답은 다음과 같습니다:

{
  "q": [
    "foo",
    "bar"
  ]
}

위의 예와 같이 list 타입으로 쿼리 매개변수를 선언하려면 Query를 명시적으로 사용해야 합니다. 그렇지 않으면 요청 본문으로 해석됩니다.

대화형 API 문서는 여러 값을 허용하도록 수정 됩니다:

쿼리 매개변수 리스트 / 기본값이 있는 다중값

제공된 값이 없으면 기본 list 값을 정의할 수도 있습니다:

from typing import Annotated

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(q: Annotated[list[str], Query()] = ["foo", "bar"]):
    query_items = {"q": q}
    return query_items
🤓 Other versions and variants

Tip

Prefer to use the Annotated version if possible.

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(q: list[str] = Query(default=["foo", "bar"])):
    query_items = {"q": q}
    return query_items

다음으로 이동하면:

http://localhost:8000/items/

q의 기본값은 ["foo", "bar"]가 되고, 응답은 다음이 됩니다:

{
  "q": [
    "foo",
    "bar"
  ]
}

list만 사용하기

list[str] 대신 list를 직접 사용할 수도 있습니다:

from typing import Annotated

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(q: Annotated[list, Query()] = []):
    query_items = {"q": q}
    return query_items
🤓 Other versions and variants

Tip

Prefer to use the Annotated version if possible.

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(q: list = Query(default=[])):
    query_items = {"q": q}
    return query_items

참고

이 경우 FastAPI는 리스트의 내용을 검사하지 않음을 명심하세요.

예를 들어, list[int]는 리스트 내용이 정수인지 검사(및 문서화)합니다. 하지만 list 단독일 경우는 아닙니다.

더 많은 메타데이터 선언

매개변수에 대한 정보를 추가할 수 있습니다.

해당 정보는 생성된 OpenAPI에 포함되고 문서 사용자 인터페이스 및 외부 도구에서 사용됩니다.

참고

도구에 따라 OpenAPI 지원 수준이 다를 수 있음을 명심하세요.

일부는 아직 선언된 추가 정보를 모두 표시하지 않을 수 있지만, 대부분의 경우 누락된 기능은 이미 개발 계획이 있습니다.

title을 추가할 수 있습니다:

from typing import Annotated

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(
    q: Annotated[str | None, Query(title="Query string", min_length=3)] = None,
):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results
🤓 Other versions and variants
from typing import Annotated, Union

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(
    q: Annotated[Union[str, None], Query(title="Query string", min_length=3)] = None,
):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results

Tip

Prefer to use the Annotated version if possible.

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(
    q: str | None = Query(default=None, title="Query string", min_length=3),
):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results

Tip

Prefer to use the Annotated version if possible.

from typing import Union

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(
    q: Union[str, None] = Query(default=None, title="Query string", min_length=3),
):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results

그리고 description도 추가할 수 있습니다:

from typing import Annotated

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(
    q: Annotated[
        str | None,
        Query(
            title="Query string",
            description="Query string for the items to search in the database that have a good match",
            min_length=3,
        ),
    ] = None,
):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results
🤓 Other versions and variants
from typing import Annotated, Union

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(
    q: Annotated[
        Union[str, None],
        Query(
            title="Query string",
            description="Query string for the items to search in the database that have a good match",
            min_length=3,
        ),
    ] = None,
):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results

Tip

Prefer to use the Annotated version if possible.

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(
    q: str | None = Query(
        default=None,
        title="Query string",
        description="Query string for the items to search in the database that have a good match",
        min_length=3,
    ),
):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results

Tip

Prefer to use the Annotated version if possible.

from typing import Union

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(
    q: Union[str, None] = Query(
        default=None,
        title="Query string",
        description="Query string for the items to search in the database that have a good match",
        min_length=3,
    ),
):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results

별칭 매개변수

매개변수가 item-query이길 원한다고 가정해 봅시다.

마치 다음과 같습니다:

http://127.0.0.1:8000/items/?item-query=foobaritems

그러나 item-query는 유효한 파이썬 변수 이름이 아닙니다.

가장 가까운 것은 item_query일 겁니다.

하지만 정확히 item-query이길 원합니다...

이럴 경우 alias를 선언할 수 있으며, 해당 별칭은 매개변수 값을 찾는 데 사용됩니다:

from typing import Annotated

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(q: Annotated[str | None, Query(alias="item-query")] = None):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results
🤓 Other versions and variants
from typing import Annotated, Union

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(q: Annotated[Union[str, None], Query(alias="item-query")] = None):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results

Tip

Prefer to use the Annotated version if possible.

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(q: str | None = Query(default=None, alias="item-query")):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results

Tip

Prefer to use the Annotated version if possible.

from typing import Union

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(q: Union[str, None] = Query(default=None, alias="item-query")):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results

매개변수 사용 중단하기

이제는 더 이상 이 매개변수를 마음에 들어하지 않는다고 가정해 봅시다.

이 매개변수를 사용하는 클라이언트가 있기 때문에 한동안은 남겨둬야 하지만, 문서에서 deprecated로 명확하게 보여주고 싶습니다.

그렇다면 deprecated=True 매개변수를 Query로 전달합니다:

from typing import Annotated

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(
    q: Annotated[
        str | None,
        Query(
            alias="item-query",
            title="Query string",
            description="Query string for the items to search in the database that have a good match",
            min_length=3,
            max_length=50,
            pattern="^fixedquery$",
            deprecated=True,
        ),
    ] = None,
):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results
🤓 Other versions and variants
from typing import Annotated, Union

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(
    q: Annotated[
        Union[str, None],
        Query(
            alias="item-query",
            title="Query string",
            description="Query string for the items to search in the database that have a good match",
            min_length=3,
            max_length=50,
            pattern="^fixedquery$",
            deprecated=True,
        ),
    ] = None,
):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results

Tip

Prefer to use the Annotated version if possible.

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(
    q: str | None = Query(
        default=None,
        alias="item-query",
        title="Query string",
        description="Query string for the items to search in the database that have a good match",
        min_length=3,
        max_length=50,
        pattern="^fixedquery$",
        deprecated=True,
    ),
):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results

Tip

Prefer to use the Annotated version if possible.

from typing import Union

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(
    q: Union[str, None] = Query(
        default=None,
        alias="item-query",
        title="Query string",
        description="Query string for the items to search in the database that have a good match",
        min_length=3,
        max_length=50,
        pattern="^fixedquery$",
        deprecated=True,
    ),
):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results

문서가 아래와 같이 보일겁니다:

OpenAPI에서 매개변수 제외

생성된 OpenAPI 스키마(따라서 자동 문서화 시스템)에서 쿼리 매개변수를 제외하려면 Queryinclude_in_schema 매개변수를 False로 설정하세요:

from typing import Annotated

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(
    hidden_query: Annotated[str | None, Query(include_in_schema=False)] = None,
):
    if hidden_query:
        return {"hidden_query": hidden_query}
    else:
        return {"hidden_query": "Not found"}
🤓 Other versions and variants
from typing import Annotated, Union

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(
    hidden_query: Annotated[Union[str, None], Query(include_in_schema=False)] = None,
):
    if hidden_query:
        return {"hidden_query": hidden_query}
    else:
        return {"hidden_query": "Not found"}

Tip

Prefer to use the Annotated version if possible.

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(
    hidden_query: str | None = Query(default=None, include_in_schema=False),
):
    if hidden_query:
        return {"hidden_query": hidden_query}
    else:
        return {"hidden_query": "Not found"}

Tip

Prefer to use the Annotated version if possible.

from typing import Union

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(
    hidden_query: Union[str, None] = Query(default=None, include_in_schema=False),
):
    if hidden_query:
        return {"hidden_query": hidden_query}
    else:
        return {"hidden_query": "Not found"}

커스텀 검증

위에 나온 매개변수들로는 할 수 없는 커스텀 검증이 필요한 경우가 있을 수 있습니다.

그런 경우에는 일반적인 검증(예: 값이 str인지 검증한 뒤) 이후에 적용되는 커스텀 검증 함수를 사용할 수 있습니다.

Annotated 안에서 Pydantic의 AfterValidator를 사용하면 이를 구현할 수 있습니다.

Pydantic에는 BeforeValidator와 같은 다른 것들도 있습니다. 🤓

예를 들어, 이 커스텀 validator는 ISBN 도서 번호의 경우 아이템 ID가 isbn-으로 시작하고, IMDB 영화 URL ID의 경우 imdb-로 시작하는지 확인합니다:

import random
from typing import Annotated

from fastapi import FastAPI
from pydantic import AfterValidator

app = FastAPI()

data = {
    "isbn-9781529046137": "The Hitchhiker's Guide to the Galaxy",
    "imdb-tt0371724": "The Hitchhiker's Guide to the Galaxy",
    "isbn-9781439512982": "Isaac Asimov: The Complete Stories, Vol. 2",
}


def check_valid_id(id: str):
    if not id.startswith(("isbn-", "imdb-")):
        raise ValueError('Invalid ID format, it must start with "isbn-" or "imdb-"')
    return id


@app.get("/items/")
async def read_items(
    id: Annotated[str | None, AfterValidator(check_valid_id)] = None,
):
    if id:
        item = data.get(id)
    else:
        id, item = random.choice(list(data.items()))
    return {"id": id, "name": item}
🤓 Other versions and variants
import random
from typing import Annotated, Union

from fastapi import FastAPI
from pydantic import AfterValidator

app = FastAPI()

data = {
    "isbn-9781529046137": "The Hitchhiker's Guide to the Galaxy",
    "imdb-tt0371724": "The Hitchhiker's Guide to the Galaxy",
    "isbn-9781439512982": "Isaac Asimov: The Complete Stories, Vol. 2",
}


def check_valid_id(id: str):
    if not id.startswith(("isbn-", "imdb-")):
        raise ValueError('Invalid ID format, it must start with "isbn-" or "imdb-"')
    return id


@app.get("/items/")
async def read_items(
    id: Annotated[Union[str, None], AfterValidator(check_valid_id)] = None,
):
    if id:
        item = data.get(id)
    else:
        id, item = random.choice(list(data.items()))
    return {"id": id, "name": item}

정보

이는 Pydantic 2 이상 버전에서 사용할 수 있습니다. 😎

데이터베이스나 다른 API 같은 외부 구성요소와 통신이 필요한 어떤 종류의 검증이든 해야 한다면, 대신 FastAPI Dependencies를 사용해야 합니다. 이에 대해서는 나중에 배우게 됩니다.

이 커스텀 validator는 요청에서 제공된 같은 데이터만으로 확인할 수 있는 것들을 위한 것입니다.

코드 이해하기

중요한 부분은 Annotated 안에서 함수와 함께 AfterValidator를 사용한다는 것뿐입니다. 이 부분은 건너뛰셔도 됩니다. 🤸


하지만 이 특정 코드 예제가 궁금하고 계속 보고 싶다면, 추가 세부사항은 다음과 같습니다.

value.startswith()를 사용한 문자열

알고 계셨나요? value.startswith()를 사용하는 문자열은 튜플을 받을 수 있으며, 튜플에 있는 각 값을 확인합니다:

# Code above omitted 👆

def check_valid_id(id: str):
    if not id.startswith(("isbn-", "imdb-")):
        raise ValueError('Invalid ID format, it must start with "isbn-" or "imdb-"')
    return id

# Code below omitted 👇
👀 Full file preview
import random
from typing import Annotated

from fastapi import FastAPI
from pydantic import AfterValidator

app = FastAPI()

data = {
    "isbn-9781529046137": "The Hitchhiker's Guide to the Galaxy",
    "imdb-tt0371724": "The Hitchhiker's Guide to the Galaxy",
    "isbn-9781439512982": "Isaac Asimov: The Complete Stories, Vol. 2",
}


def check_valid_id(id: str):
    if not id.startswith(("isbn-", "imdb-")):
        raise ValueError('Invalid ID format, it must start with "isbn-" or "imdb-"')
    return id


@app.get("/items/")
async def read_items(
    id: Annotated[str | None, AfterValidator(check_valid_id)] = None,
):
    if id:
        item = data.get(id)
    else:
        id, item = random.choice(list(data.items()))
    return {"id": id, "name": item}
🤓 Other versions and variants
import random
from typing import Annotated, Union

from fastapi import FastAPI
from pydantic import AfterValidator

app = FastAPI()

data = {
    "isbn-9781529046137": "The Hitchhiker's Guide to the Galaxy",
    "imdb-tt0371724": "The Hitchhiker's Guide to the Galaxy",
    "isbn-9781439512982": "Isaac Asimov: The Complete Stories, Vol. 2",
}


def check_valid_id(id: str):
    if not id.startswith(("isbn-", "imdb-")):
        raise ValueError('Invalid ID format, it must start with "isbn-" or "imdb-"')
    return id


@app.get("/items/")
async def read_items(
    id: Annotated[Union[str, None], AfterValidator(check_valid_id)] = None,
):
    if id:
        item = data.get(id)
    else:
        id, item = random.choice(list(data.items()))
    return {"id": id, "name": item}

임의의 항목

data.items()를 사용하면 각 딕셔너리 항목의 키와 값을 담은 튜플로 구성된 iterable object를 얻습니다.

이 iterable object를 list(data.items())로 적절한 list로 변환합니다.

그 다음 random.choice()로 리스트에서 무작위 값을 얻어 (id, name) 형태의 튜플을 얻습니다. 예를 들어 ("imdb-tt0371724", "The Hitchhiker's Guide to the Galaxy") 같은 값이 될 것입니다.

그 다음 이 튜플의 두 값을 변수 idname할당합니다.

따라서 사용자가 아이템 ID를 제공하지 않더라도, 무작위 추천을 받게 됩니다.

...이 모든 것을 단 하나의 간단한 줄로 합니다. 🤯 Python 정말 좋지 않나요? 🐍

# Code above omitted 👆

@app.get("/items/")
async def read_items(
    id: Annotated[str | None, AfterValidator(check_valid_id)] = None,
):
    if id:
        item = data.get(id)
    else:
        id, item = random.choice(list(data.items()))
    return {"id": id, "name": item}
👀 Full file preview
import random
from typing import Annotated

from fastapi import FastAPI
from pydantic import AfterValidator

app = FastAPI()

data = {
    "isbn-9781529046137": "The Hitchhiker's Guide to the Galaxy",
    "imdb-tt0371724": "The Hitchhiker's Guide to the Galaxy",
    "isbn-9781439512982": "Isaac Asimov: The Complete Stories, Vol. 2",
}


def check_valid_id(id: str):
    if not id.startswith(("isbn-", "imdb-")):
        raise ValueError('Invalid ID format, it must start with "isbn-" or "imdb-"')
    return id


@app.get("/items/")
async def read_items(
    id: Annotated[str | None, AfterValidator(check_valid_id)] = None,
):
    if id:
        item = data.get(id)
    else:
        id, item = random.choice(list(data.items()))
    return {"id": id, "name": item}
🤓 Other versions and variants
import random
from typing import Annotated, Union

from fastapi import FastAPI
from pydantic import AfterValidator

app = FastAPI()

data = {
    "isbn-9781529046137": "The Hitchhiker's Guide to the Galaxy",
    "imdb-tt0371724": "The Hitchhiker's Guide to the Galaxy",
    "isbn-9781439512982": "Isaac Asimov: The Complete Stories, Vol. 2",
}


def check_valid_id(id: str):
    if not id.startswith(("isbn-", "imdb-")):
        raise ValueError('Invalid ID format, it must start with "isbn-" or "imdb-"')
    return id


@app.get("/items/")
async def read_items(
    id: Annotated[Union[str, None], AfterValidator(check_valid_id)] = None,
):
    if id:
        item = data.get(id)
    else:
        id, item = random.choice(list(data.items()))
    return {"id": id, "name": item}

요약

매개변수에 검증과 메타데이터를 추가 선언할 수 있습니다.

제네릭 검증과 메타데이터:

  • alias
  • title
  • description
  • deprecated

문자열에 특화된 검증:

  • min_length
  • max_length
  • pattern

AfterValidator를 사용하는 커스텀 검증.

예제에서 str 값의 검증을 어떻게 추가하는지 살펴보았습니다.

숫자와 같은 다른 타입에 대한 검증을 어떻게 선언하는지 확인하려면 다음 장을 확인하기 바랍니다.