쿼리 파라미터와 문자열 검증

쿼리 파라미터와 문자열 검증 (Query Parameters and String Validations)

FastAPI를 사용하면 파라미터에 대해 추가 정보와 검증(validation)을 선언할 수 있습니다.

이 애플리케이션을 예로 들어 봅시다:

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

쿼리 파라미터 q의 타입은 str | None입니다. 즉 str 타입이지만 None일 수도 있다는 뜻입니다. 실제로 기본값이 None이므로, FastAPI는 이 파라미터가 필수가 아니라는 것을 알게 됩니다.

!!! note "참고" FastAPI는 = None 기본값 덕분에 q의 값이 필수가 아니라는 것을 알게 됩니다.

`str | None` 타입을 쓰면 편집기가 더 나은 지원을 제공하고 오류를 감지하는 데도 도움이 됩니다.

출처: 공식문서

추가 검증 (Additional validation)

q는 선택 사항이지만, 제공될 때마다 그 길이가 50자를 넘지 않도록 강제해 보겠습니다.

QueryAnnotated 임포트하기 (Import Query and Annotated)

그렇게 하려면 먼저 다음을 임포트하세요:

  • 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

🤓 다른 버전과 변형

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

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

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

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

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

q 파라미터의 타입에 Annotated 사용하기 (Use Annotated in the type for the q parameter)

Python Types Intro에서 Annotated가 파라미터에 메타데이터를 추가하는 데 사용될 수 있다고 말한 걸 기억하시나요?

이제 그것을 FastAPI와 함께 쓸 때입니다. 🚀

우리는 이 타입 어노테이션을 갖고 있었지요:

q: str | None = None

우리가 할 일은 그것을 Annotated로 감싸는 것, 그래서 이렇게 됩니다:

q: Annotated[str | None] = None

두 버전 모두 같은 뜻입니다. qstr 또는 None이 될 수 있는 파라미터이고, 기본값은 None이라는 뜻이죠.

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

q 파라미터의 AnnotatedQuery 추가하기 (Add Query to Annotated in the q parameter)

이제 더 많은 정보(여기서는 추가 검증)를 넣을 수 있는 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

🤓 다른 버전과 변형

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

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

기본값이 여전히 None이므로 파라미터는 여전히 선택 사항입니다.

하지만 이제 Annotated 안에 Query(max_length=50)이 있으므로, 이 값에 추가 검증을 원한다는 것, 즉 최대 50자를 원한다는 것을 FastAPI에 알려 주는 것입니다. 😎

!!! tip "팁" 여기서는 이것이 쿼리 파라미터이기 때문에 Query()를 사용합니다. 나중에 Query()와 같은 인자를 받는 Path(), Body(), Header(), Cookie() 같은 다른 것들도 보게 될 거예요.

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

  • 데이터를 검증해서 최대 길이가 50자인지 확인
  • 데이터가 유효하지 않을 때 클라이언트에게 명확한 오류 표시
  • OpenAPI 스키마 경로 동작 에 파라미터를 문서화(그래서 자동 문서 UI에 표시)

대체(구식) 방법: Query를 기본값으로 (Alternative old: Query as the default value)

이전 버전의 FastAPI(0.95.0 이전)는 Annotated에 넣는 대신 Query를 파라미터의 기본값으로 사용해야 했습니다. 그걸 사용하는 코드를 볼 가능성이 높으니 설명해 드릴게요.

!!! tip "팁" 새 코드에서는, 그리고 가능하면, 위에서 설명한 대로 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

🤓 다른 버전과 변형

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

이 경우(Annotated를 쓰지 않을 때) 함수의 None 기본값을 Query()로 바꿔야 하므로, 이제 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에 넣을지 (Query as the default value or in Annotated)

Annotated 안에서 Query를 사용할 때는 Querydefault 파라미터를 사용할 수 없다는 것을 명심하세요.

대신 함수 파라미터의 실제 기본값을 사용하세요. 그렇지 않으면 일관성이 없어집니다.

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

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

...기본값이 "rick"이어야 하는지 "morty"여야 하는지 명확하지 않기 때문입니다.

그래서 (가급적) 이렇게 쓰세요:

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

...또는 오래된 코드베이스에서는 이렇게 찾을 수 있습니다:

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

Annotated의 장점 (Advantages of Annotated)

함수 파라미터의 기본값 대신 Annotated를 사용하는 것이 권장됩니다. 여러 이유로 더 낫기 때문이죠. 🤓

함수 파라미터의 기본값실제 기본값이므로, 일반적인 Python과 더 직관적입니다. 😌

그 같은 함수를 FastAPI 없이 다른 곳에서 호출해도 예상대로 동작합니다. 필수 파라미터(기본값이 없는)가 있으면 편집기가 오류로 알려 주고, Python도 필수 파라미터를 넘기지 않고 실행하면 불평합니다.

Annotated를 쓰지 않고 (구식) 기본값 방식을 쓰면, FastAPI 없이 다른 곳에서 그 함수를 호출할 때, 정상적으로 동작하도록 인자를 꼭 넘겨야 한다는 것을 기억해야 합니다. 그렇지 않으면 값이 예상과 달라져요(예: str이 아니라 QueryInfo 같은 것). 그리고 편집기도 불평하지 않고, Python도 그 함수를 실행할 때 불평하지 않습니다. 단지 내부 연산이 오류 날 때만 그렇죠.

Annotated는 메타데이터 어노테이션을 여러 개 가질 수 있으므로, Typer 같은 다른 도구와도 같은 함수를 쓸 수 있습니다. 🚀

더 많은 검증 추가하기 (Add more validations)

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

🤓 다른 버전과 변형

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

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

정규 표현식 추가하기 (Add regular expressions)

파라미터가 매칭해야 하는 정규 표현식 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

🤓 다른 버전과 변형

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

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

이 특정 정규 표현식 패턴은 수신된 파라미터 값이 다음을 만족하는지 확인합니다:

  • ^: 뒤따르는 문자들로 시작하고, 그 앞에 문자가 없음.
  • fixedquery: 정확히 fixedquery라는 값을 가짐.
  • $: 거기서 끝나고, fixedquery 뒤에 더 이상 문자가 없음.

이런 "정규 표현식" 개념들이 헷갈린다고 해도 걱정하지 마세요. 많은 사람에게 어려운 주제입니다. 아직 정규 표현식 없이도 많은 것을 할 수 있어요.

이제 필요할 때 FastAPI에서 쓸 수 있다는 걸 알게 된 것입니다.

기본값 (Default values)

물론 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

🤓 다른 버전과 변형

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

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

!!! note "참고" None을 포함한 어떤 타입의 기본값이 있으면 파라미터가 선택 사항(필수 아님)이 됩니다.

필수 파라미터 (Required parameters)

더 이상 검증이나 메타데이터를 선언할 필요가 없을 때는, 기본값을 선언하지 않음으로써 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

🤓 다른 버전과 변형

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

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 가능 (Required, can be 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

🤓 다른 버전과 변형

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

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

쿼리 파라미터 리스트 / 여러 값 (Query parameter list / multiple values)

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

🤓 다른 버전과 변형

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

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

그런 다음 이 같은 URL을 쓰면:

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

여러 q 쿼리 파라미터 의 값(foobar)을 Python list경로 동작 함수 안의 함수 파라미터 q로 받게 됩니다.

그래서 그 URL에 대한 응답은 이렇게 됩니다:

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

!!! tip "팁" 위 예제처럼 list 타입의 쿼리 파라미터를 선언하려면 명시적으로 Query를 사용해야 합니다. 그렇지 않으면 요청 바디로 해석됩니다.

대화형 API 문서도 여러 값을 허용하도록 그에 맞게 업데이트됩니다.

기본값이 있는 쿼리 파라미터 리스트 / 여러 값 (Query parameter list / multiple values with defaults)

아무것도 제공되지 않으면 기본 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

🤓 다른 버전과 변형

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

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 사용하기 (Using just 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

🤓 다른 버전과 변형

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

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

!!! note "참고" 이 경우 FastAPI는 리스트의 내용을 확인하지 않는다는 점을 명심하세요.

예를 들어 `list[int]`는 리스트의 내용이 정수인지 확인(하고 문서화)합니다. 하지만 그냥 `list`는 그렇지 않습니다.

더 많은 메타데이터 선언하기 (Declare more metadata)

파라미터에 대해 더 많은 정보를 추가할 수 있습니다.

그 정보는 생성된 OpenAPI에 포함되며, 문서 UI와 외부 도구에서 사용됩니다.

!!! note "참고" 서로 다른 도구들은 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

🤓 다른 버전과 변형

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

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

그리고 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

🤓 다른 버전과 변형

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

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

별칭 파라미터 (Alias parameters)

파라미터를 item-query로 만들고 싶다고 상상해 봅시다.

이렇게요:

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

하지만 item-query는 유효한 Python 변수 이름이 아닙니다.

가장 비슷한 것은 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

🤓 다른 버전과 변형

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

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

파라미터 폐기하기 (Deprecating parameters)

이제 이 파라미터가 더 이상 마음에 들지 않는다고 합시다.

사용하는 클라이언트가 있으니 잠시 두어야 하지만, 문서에 폐기된 것으로 명확히 표시하고 싶어요.

그럴 때 Querydeprecated=True 파라미터를 넘기면 됩니다:

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

🤓 다른 버전과 변형

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

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

문서에는 이렇게 표시됩니다.

OpenAPI에서 파라미터 제외하기 (Exclude parameters from 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"}

🤓 다른 버전과 변형

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

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"}

커스텀 검증 (Custom Validation)

위에서 보여준 파라미터들로는 할 수 없는 커스텀 검증이 필요한 경우가 있을 수 있습니다.

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

Annotated 안에서 Pydantic의 AfterValidator를 사용하면 됩니다.

!!! tip "팁" Pydantic에는 BeforeValidator 같은 것들도 있습니다. 🤓

예를 들어, 이 커스텀 검증자는 항목 ID가 ISBN 도서 번호의 경우 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}

!!! note "참고" 이 기능은 Pydantic 버전 2 이상에서 사용할 수 있습니다. 😎

!!! tip "팁" 데이터베이스나 다른 API 같은 어떤 외부 컴포넌트와 통신해야 하는 검증이 필요하다면, 대신 FastAPI Dependencies를 사용해야 합니다. 나중에 배우게 될 거예요.

이런 커스텀 검증자는 요청에서 제공된 **같은 데이터만**으로 확인할 수 있는 것들에 사용합니다.

그 코드 이해하기 (Understand that Code)

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


하지만 이 특정 코드 예제가 궁금하고 아직 재미있다면, 몇 가지 추가 설명이 있습니다.

value.startswith()가 있는 문자열 (String with 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 👇

👀 전체 파일 미리보기

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}

임의의 항목 (A Random Item)

data.items()로 우리는 각 딕셔너리 항목의 키와 값을 담고 있는 튜플들이 있는 반복 가능한(iterable) 객체를 얻습니다.

이 반복 가능한 객체를 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}

👀 전체 파일 미리보기

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}

요약 (Recap)

파라미터에 대한 추가 검증과 메타데이터를 선언할 수 있습니다.

일반적인 검증과 메타데이터:

  • alias
  • title
  • description
  • deprecated

문자열에 특화된 검증:

  • min_length
  • max_length
  • pattern

AfterValidator를 사용한 커스텀 검증.

이 예제들에서 str 값에 대한 검증을 선언하는 방법을 봤습니다.

숫자 같은 다른 타입에 대한 검증 선언 방법은 다음 장들에서 배웁니다.

더 알아보기 (Learn more)

  • Query(...)로 문자열 검증(min_length, max_length, pattern)과 메타데이터(title, description, alias, deprecated, include_in_schema)를 선언할 수 있습니다.
  • Annotated 안에 Query를 넣는 방식이 권장되며, 기본값은 함수 파라미터의 실제 기본값으로 둡니다.
  • list[str] 타입으로 여러 값(리스트)을 받을 수 있고, AfterValidator로 커스텀 검증을 추가할 수 있습니다.