8 minute read

1. Query Parameters and String Validations

1.1 Query Parameter의 Validation이란?

Validation은 Client가 전달한 데이터가 API가 요구하는 타입과 조건을 만족하는지 확인하는 과정입니다.

Query Parameter 역시 Client가 Server로 전달하는 외부 입력이므로, 단순히 값을 받아 사용하는 것이 아니라 해당 값이 API에서 기대하는 조건에 맞는지 검증할 필요가 있습니다.

예를 들어 Query Parameter가 문자열이라면 단순히 str 타입인지만 확인하는 것이 아니라 다음과 같은 추가 조건을 설정할 수 있습니다.

  • 최소 문자열 길이
  • 최대 문자열 길이
  • 특정 문자열 패턴

이러한 검증을 통해 잘못된 데이터가 실제 Business Logic까지 전달되기 전에 요청을 차단할 수 있습니다.

1.2 Query()를 이용한 문자열 검증

FastAPI에서는 Query()를 이용하여 Query Parameter에 추가적인 Validation 조건을 선언할 수 있습니다.

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)]
):
  return {"q": q}

위 코드에서 각 부분의 역할은 다음과 같습니다.

str | None
→ Query Parameter의 타입

Query(min_length=3, max_length=50)
→ 추가적인 Validation 조건

= None
→ 기본값
→ Optional Parameter

min_length=3은 문자열의 길이가 최소 3 이상이어야 한다는 의미이며, max_length=50은 문자열의 최대 길이를 50까지 허용한다는 의미입니다. 문자열 검증에는 대표적으로 다음과 같은 조건을 사용할 수 있습니다.

Validation 의미
min_length 최소 문자열 길이
max_length 최대 문자열 길이
pattern 문자열이 특정 패턴을 만족하는지 검증

FastAPI에서는 이러한 조건을 선언하면 직접 if문을 작성하지 않아도 요청 데이터를 자동으로 검증합니다.

Client Request ↓ Query Parameter 추출 ↓ 타입 확인 ↓ Query()에 선언된 추가 조건 확인 ↓ Validation 성공 ↓ Path Operation Function 실행

Validation에 실패하면 해당 요청은 Path Operation Function까지 전달되지 않고 FastAPI가 오류 Response를 반환합니다.

1.3 Validation과 Metadata

Query()에는 Validation뿐만 아니라 Query Parameter에 대한 Metadata도 추가할 수 있습니다. 예를 들면 다음과 같이 작성 할 수 있습니다.

q: Annotated[ 
  str | None, 
  Query( 
    min_length=3, 
    max_length=50, 
    title="Search query", 
    description="검색에 사용할 문자열" 
  ) 
] = None

이때 각각의 역할은 다음과 같습니다.

min_length / max_length
→ Validation

title / description
→ Metadata

Validation은 실제 요청 데이터가 조건을 만족하는지 검사하는데 사용됩니다. 반면 Metadata는 Parameter가 어떤 의미를 가지는지 설명하기 위한 추가 정보이며, FastAPI가 생성하는 OpenAPI Schema와 API 문서에도 활용됩니다.

대표적으로 Query()에서는 다음과 같은 Metadata를 설정할 수 있습니다.

  • title
  • description
  • alias
  • deprecated

따라서 Query()는 단순한 데이터 검증 도구가 아니라 Query Parameter의 검증 조건과 부가 정보를 함께 선언하기 위한 기능이라고 볼 수 있습니다.

1.4 Required / Optional Query Parameter

Query Parameter가 필수인지 선택 사항인지는 기본값의 존재 여부에 따라 결정됩니다.

다음과 같이 기본값이 없다면:

q: str

q는 반드시 전달해야 하는 Required Parameter가 됩니다.

반면 기본값을 지정하면:

q: str = "fastapi"

Client가 값을 전달하지 않았을 때 "fastapi"가 사용되므로 Optional Parameter가 됩니다.

또한 다음과 같이 None을 기본값으로 사용할 수도 있습니다.

q: str | None = None

이 경우 Client가 q를 전달하지 않으면 None이 사용됩니다.

Annotated를 사용하는 경우에도 동일합니다.

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

여기서 Query(min_length=3)은 Validation 조건을 추가하지만, Parameter가 Required인지 Optional인지를 결정하는 것은 =None이라는 기본값입니다.

따라서 다음 두 개념을 구분해서 이해해야 합니다.

str | None
→ 값으로 None을 허용하는 타입 정보

= None
→ 값을 전달하지 않았을 때 사용할 기본값
→ Parameter를 Optional하게 만드는 요소

1.5 정리

Query Parameters and String Validations의 핵심은 다음과 같이 정리할 수 있습니다.

FastAPI에서는 Query()를 이용하여 Query Parameter에 문자열 길이, 패턴 등의 추가적인 Valiation 조건을 선언할 수 있으며, Metadata도 함께 설정할 수 있습니다. 이러한 Validation은 잘못된 입력이 실제 처리 로직에 전달되지 전에 요청 데이터를 검증하는 역할을 합니다.

2. Path Parameters and Numeric Validations

2.1 Path()를 이용한 Path Parameter 검증

FastAPI에서는 Query Parameter에 Query()를 사용하는 것처럼, Path Parameter에는 Path()를 사용하여 Validation과 Metadata를 추가할 수 있습니다.

기본적인 Path Parameter는 다음과 같이 선언합니다.

@app.get("/items/{item_id}")
async def read_item(item_id: int):
  return {"item_id": item_id}

여기에 추가적인 검증 조건을 적용하려면 Path()를 사용할 수 있습니다.

from typing import Annotated
from fastapi import Path

item_id: Annoateted[int, Path(gt=0)]

위 선언은 item_id가 단순히 int 타입이어야 할 뿐만 아니라, 0보다 큰 값이어야 한다는 조건까지 포함합니다.

Path() 역시 Query()와 마찬가지로 title, description 등의 Metadata를 함께 설정할 수 있습니다.

2.2 Path Paramter가 항상 필수인 이유

Query Parameter는 기본값을 설정하여 Optional Parameter로 만들 수 있지만, Path Parameter는 항상 Required Parameter입니다. 그 이유는 Path Parameter가 URL 자체의 일부이기 때문입니다.

예를 들어 다음과 같은 Path Operation이 있다고 하겠습니다. 0

@app.get("/item/{item_id}")

여기서 /items/10은 위 경로와 일치하지만 /items/item_id가 생략된 동일한 요청이 아니라 다른 Path가 됩니다. 즉,

/items/{item_id}
↑ URL 구조 자체의 일부

이기 때문에 Path Parameter는 생략할 수 없습니다. 따라서 Path Parameter에 None이나 특정 기본값ㅇ르 지정하더라도 Query Parameter처럼 Optional Parameter가 되는 것은 아닙니다.

2.3 Numeric Validation

FastAPI에서는 숫자형 Parameter에 대해 값의 범위르 제한하는 Numeric Valiation을 사용할 수 있습니다. 대표적으로 네 가지 조건이 있습니다.

Validation 의미
gt 지정한 값보다 커야함 (>)
ge 지정한 값보다 크거나 같아야 함 (>=)
lt 지정한 값보다 작아야 함 (<)
le 지정한 값보다 작거나 같아야 함 (<=)

예를 들어 다음과 같이 선언할 수 있습니다.

item_id: Annotated[int, Path(gt=0, le=10000)]

이는 다음 조건을 의미합니다.

0 < item_id <= 1000

단순히 item_id: int라고 선언하면 정수 타입인지만 확인할 수 있지만, Path()에 Numeric Validation을 추가하면 허용 가능한 값의 범위까지 검증할 수 있습니다.

이러한 검증을 통해 API가 허용하지 않는 숫자 값이 실제 처리로직까지 전달되는 것을 방지할 수 있습니다. Numeric Valiation은 int뿐만 아니라 float에도 사용할 수 있습니다.

2.4 Query()와 Path()의 Validation 관계

FastAPI 공식 문서에서는 Query Parameters and String ValidationsPath Parameters and Numeric Validations를 서로 다른 섹션으로 설명하고 있지만, 이를 다음과 같이 이해해서는 안됩니다.

Query Parameter → 문자열만 검증
Path Parameter → 숫자만 검증

문자열 Validation과 Numeric Validation은 특정 Parameter 종류에 종속된 기능이 아닙니다. Query()와 Path() 모두 데이터 타입에 맞는 Validation 조건을 추가할 수 있습니다.

2.5 정리

Path Parameters and Numeric Validations의 핵심은 다음과 같이 정리할 수 있습니다.

FastAPI에서는 Path()를 이용하여 Path Parameter에 Validation과 Metadata를 추가할 수 있습니다. Path Parameter는 URL Path 자체의 일부이므로 항상 필수이며, gt, ge, lt, le 등의 Numeric Validation을 이용하여 숫자 데이터의 허용 범위를 제한할 수 있습니다. 이러한 Numeric Validation은 Path Parameter뿐만 아니라 Query Parameter에도 적용할 수 있습니다.

3. Body - Fields

3.1 Pydantic Model의 Fields란?

Pydantic Model에서 각각의 데이터 항목을 Field라고 볼 수 있습니다. 예를 들어 다음과 같은 모델이 있다면

class Item(BaseModel):
  name: str
  price: float

여기서 nameprice가 각각 하나의 Field입니다. 즉, Pydantic Model은 여러 개의 Field로 구성되며 각 Field는 자신이 가져야 할 타입과 조건을 가질 수 있습니다.

3.2 Field()를 이용한 Validation과 Metadata

Pydantic에서는 Field()를 이용하여 Model 내부의 각 Field에 추가적인 Validation과 Metadat를 설정할 수 있습니다.

from pydantic import BaseModel, Field

class Item(BaseModel):
  name: str
  price: float = Field(gt=0, description="상품 가격")

위 코드에서:

  • float은 price의 타입을 의미합니다
  • gt=0은 값이 0보다 커야 한다는 Validation 조건입니다.
  • description은 해당 Field에 대한 Metadata입니다.

Field()는 단순히 데이터 타입을 정의하는 것을 넘어 각 Field가 만족해야 하는 세부 조건과 설명 정보를 함께 정의할 때 사용합니다.

3.3 Query(), Path(), Body(), Field()의 차이

지금까지 학습한 Query(), Path(), Body(), Field()는 모두 데이터를 선언하고 검증하는 데 사용되지만 적용되는 위치는 다릅니다.

Query()
→ Query Parameter

Path()
→ Path Parameter

Body()
→ Request Body Parameter

Field()
→ Pydantic Model 내부의 개별 Field

특히 Field()는 FastAPI가 아니라 Pydantic에서 제공하는 기능이라는 점이 중요합니다. FastAPI는 Pydantic Model과 Field()에 정의된 정보를 활용하여 Request Body를 검증하고 OpenAPI Schema 및 API 문서를 생성합니다.

3.4 정리

Body - Fields의 핵심은 다음과 같이 정리할 수 있습니다.

Pydantic Model 내부의 각 데이터 항목을 Field라고 하며, Field()를 이용하면 각 Field에 Validation 조건과 Metadata를 추가할 수 있습니다. Query()와 Path()가 Query Parameter와 Path Parameter를 대상으로 한다면, Field()는 Pydantic Model 내부의 개별 데이터 항목을 대상으로 합니다.

4. Body - Nested Models

4.1 Nested Model이란?

Nested Model은 Pydantic Model 내부의 Field 타입으로 또 다른 Pydantic Model을 사용하는 구조를 의미합니다. 예시로 설명을 하자면 다음과 같이 Item Model 내부에 Image Model을 포함할 수 있습니다.

from pydnatic import BaseModel

class Image(BaseModel):
  url: str
  name: str

class Item(BaseModel):
  name: str
  price: float
  image: Image

이 경우 Item의 데이터 구조는 다음과 같이 중첩됩니다.

Item
├─ name: str
├─ price: float
└─ image: Image
├─ url: str
└─ name: str

따라서 Request Body의 JSON 역시 Model 구조에 맞게 중첩된 형태를 가지게 됩니다. Nested Model을 이용하면 실제 API에서 자주 사용하는 족잡하고 계층적인 JSON 구조를 Pydantic Model로 표현할 수 있습니다.

4.2 Collection 내부 타입 정의

Pydantic에서는 list, set과 같은 Collection의 내부 데이터 타입도 함께 정의할 수 있습니다.

tags: list[str]

위 코드는 단순히 tagslist라는 의미뿐만 아니라, 리스트 내부의 각 값이 str 타입이어야 한다는 의미까지 포함합니다.

마찬가지로 다음과 같은 선언도 가능합니다.

images: list[Image]

이는 images가 리스트이며, 리스트의 각 원소가 모두 Image Model의 구조를 만족해야 한다는 의미입니다.

4.3 Pydnatic Model 중첩하기

Nested Model은 한 단계뿐만 아니라 여러 단계로 중첩할 수 있습니다. 개념적으로 다음과 같은 구조도 표현할 수 있습니다.

Offer
└─ items: list[Item]
└─ Item
└─ images: list[Image]
└─ Image

즉, 복잡한 JSON 데이터가 여러 계층으로 구성되어 있더라도 Pydantic Model을 이용하여 같은 구조를 Python 코드로 표현할 수 있습니다. 이를 통해 데이터 구조가 복잡해지더라도 각 Model의 역할을 분리하고, 전체 데이터 구조를 명확하게 정의할 수 있습니다.

4.4 list[Model]과 중첩 데이터 검증

Nested Model의 중요한 특징은 Pydantic이 최상위 데이터만 확인하는 것이 아니라 중첩된 내부 데이터까지 검증한다는 점입니다.

images: list[Image]

Pydantic은 위 코드를 다음과 같은 과정을 통해 데이터를 검증 합니다.

images가 list인가?
↓ 각 원소가 Image 구조를 만족하는가?
↓ 각 Image 내부 Field의 타입과 조건이 올바른가?
↓ 전체 Validation 성공

즉, list[Image]는 단순히 리스트인지 확인하는 것이 아니라 리스트 내부의 각 객체까지 Image Model을 기준으로 검증합니다. 이러한 방식으로 Pydantic은 중첩된 데이터 구조를 따라가며 각 단계의 타입과 Validation 조건을 확인할 수 있습니다.

4.5 dict와 Nested Model의 차이

복잡한 Request Body를 단순한 dict로 받을 수도 있지만, 내부 데이터 구조가 명확한 경우에는 Nested Model을 사용하는 것이 더 좋습니다.

dict로 데이터를 받으면 Dictionary라는 사실은 알 수 있지만, 내부에 어떤 Key와 타입이 있어야 하는지 명확하게 표현하기가 어렵습니다.

반면 Nested Model을 사용하면 다음과 같이 데이터의 구조 자체를 코드로 정의할 수 있습니다.

Item
├─ name: str
├─ price: float
└─ image: Image
├─ url: str
└─ name: str

두 방식의 차이는 다음과 같이 정리할 수 있습니다.

dict Nested Pydantic Model
내부 구조가 상대적으로 명확하지 않음 내부 구조를 명확하게 정의
내부 Key와 타입을 별도로 확인해야 할 수 있음 중첩된 Field까지 자동 Validation
타입 정보 활용이 제한적 Python Type Hint 활용 가능
API Schema가 구체적이지 않을 수 있음 구체적인 OpenAPI Schema 생성 가능
Dictionary Key로 데이터 접근 Python 객체의 Attribute로 접근 가능

따라서 데이터 구조가 명확하게 정의된 API에서는 단순한 dict 보다 Pydantic Model을 계층적으로 구성하는 것이 데이터 구조와 Valiation 규칙을 명확하게 표현하는데 유리합니다.

4.6 정리

Body - Nested Models의 핵심은 다음과 같이 정리할 수 있습니다.

Pydantic에서는 Model 내부에 다른 Model이나 list[Model]과 같은 Collection을 포함하여 복잡한 JSON 구조를 표현할 수 있습니다. 또한 Pydantic은 중첩된 구조를 따라 내부 데이터까지 Validation하므로, 복잡한 Request Body도 명확한 타입과 구조를 가진 Python 객체로 처리할 수 있습니다.

Comments