개발/FastAPI

FastAPI Pydantic 모델 기초: 요청 데이터를 검증하는 방법

notebase 2026. 5. 26. 09:28

FastAPI에서 Pydantic 모델을 사용하는 이유와 BaseModel 작성법, POST 요청 데이터 검증, Swagger UI 문서 자동 생성 흐름을 초보자 기준으로 정리합니다.

 

FastAPI Pydantic 모델은 API로 들어오는 JSON 데이터를 정해진 구조로 받고, 타입이 맞는지 자동으로 검증할 때 사용한다. POST 요청을 다루기 시작했다면 거의 반드시 만나게 되는 개념이다.

FastAPI를 처음 배울 때는 보통 이런 코드부터 시작한다.

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

 

이 단계에서는 URL에 들어온 값을 함수 인자로 받는다.

그런데 회원가입, 게시글 작성, 상품 등록처럼 여러 값을 한 번에 보내야 하는 상황은 다르다.

예를 들어 클라이언트가 이런 데이터를 보낸다고 해보자.

{
  "name": "맥북 파우치",
  "price": 25000,
  "is_active": true
}

 

이 데이터는 URL 경로에 넣기 어렵다. 보통 HTTP 요청의 본문, 즉 Request Body에 담아서 보낸다.

FastAPI 공식 문서에서도 요청 본문을 선언할 때 Pydantic 모델을 사용한다고 설명한다. FastAPI는 이 모델을 기반으로 데이터 검증과 문서화를 처리한다.

참고: [FastAPI 공식 문서 - Request Body](https://fastapi.tiangolo.com/tutorial/body/)


 

Pydantic 모델은 무엇인가

Pydantic은 파이썬 타입 힌트를 이용해 데이터 구조를 정의하고, 들어온 데이터가 그 구조에 맞는지 검증하는 라이브러리다.

이 글의 예제는 2026년 5월 기준 FastAPI와 Pydantic v2 문법을 기준으로 작성했다. 오래된 튜토리얼에서 보이는 Pydantic v1 문법이나 에러 메시지 형태와 일부 차이가 있을 수 있다.

FastAPI에서는 Pydantic을 API 요청과 응답 처리에 적극적으로 활용한다.

쉽게 보면 Pydantic 모델은 이런 역할을 한다.

클라이언트가 보낸 JSON
        ↓
Pydantic 모델로 구조 확인
        ↓
타입이 맞으면 FastAPI 함수로 전달
        ↓
타입이 틀리면 자동으로 에러 응답

 

직접 if price < 0, if name is None 같은 코드를 매번 작성하지 않아도 된다.

여기서 중요한 점은 Pydantic 모델이 단순히 데이터를 담는 그릇이 아니라는 것이다. API가 받을 수 있는 데이터의 모양을 정하고, 잘못된 요청을 함수 실행 전에 걸러내는 기준이 된다.

참고: [Pydantic 공식 문서 - Models](https://pydantic.dev/docs/validation/latest/concepts/models/)


 

BaseModel로 요청 데이터 구조 만들기

Pydantic 모델은 보통 BaseModel을 상속해서 만든다.

from pydantic import BaseModel

class Item(BaseModel):
    name: str
    price: int
    is_active: bool

 

이 코드는 Item이라는 데이터 구조를 만든 것이다.

각 필드는 다음 의미를 가진다.

필드 타입 의미
name str 문자열
price int 정수
is_active bool 참/거짓 값

 

여기서 중요한 점은 이 모델이 실제로 검증 규칙으로도 사용된다는 것이다.

예를 들어 price에 문자열이 들어오거나, 필수 값인 name이 빠지면 FastAPI가 요청을 정상 처리하지 않고 검증 에러를 반환한다.


 

POST 요청에서 Pydantic 모델 사용하기

이제 FastAPI 코드에 연결해보자.

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
    name: str
    price: int
    is_active: bool

@app.post("/items")
def create_item(item: Item):
    return item

 

핵심은 이 부분이다.

def create_item(item: Item):

 

item의 타입을 Item으로 지정했기 때문에 FastAPI는 요청 본문을 Item 모델에 맞춰 해석한다.

클라이언트가 아래처럼 요청을 보내면 정상 처리된다.

{
  "name": "맥북 파우치",
  "price": 25000,
  "is_active": true
}

 

응답은 대략 이렇게 나온다.

{
  "name": "맥북 파우치",
  "price": 25000,
  "is_active": true
}

 

실제로는 이 데이터를 데이터베이스에 저장하거나, 추가 로직을 거친 뒤 응답하게 된다. 지금은 Pydantic 모델 흐름을 보기 위해 그대로 반환하는 예시다.


 

Swagger UI에서도 자동으로 보인다

FastAPI의 편한 점은 Pydantic 모델을 작성하면 Swagger UI 문서에도 요청 본문 구조가 자동으로 반영된다는 점이다.

서버를 실행한 뒤 아래 주소로 들어가면 된다.

http://127.0.0.1:8000/docs

 

POST /items를 열어보면 name, price, is_active 필드가 포함된 요청 예시가 보인다.

이 부분이 입문자에게 꽤 중요하다.

Pydantic 모델을 작성하는 것은 단순히 파이썬 코드 내부에서만 의미가 있는 게 아니다. API를 사용하는 사람에게 “이 API는 이런 JSON을 받는다”는 문서 역할도 같이 한다.


 

타입 검증은 어떻게 동작할까

다음처럼 잘못된 요청을 보낸다고 해보자.

{
  "name": "맥북 파우치",
  "price": "비쌈",
  "is_active": true
}

 

priceint로 선언했는데 문자열 "비쌈"이 들어왔다.

이 경우 FastAPI는 함수를 실행하기 전에 요청을 막고 검증 에러를 반환한다. 개발자가 직접 예외 처리를 작성하지 않아도 된다.

보통 이런 식의 응답을 볼 수 있다.

{
  "detail": [
    {
      "type": "int_parsing",
      "loc": ["body", "price"],
      "msg": "Input should be a valid integer",
      "input": "비쌈"
    }
  ]
}

 

에러 메시지를 처음 보면 길어 보이지만, 핵심은 간단하다.

body 안의 price 값이 int로 변환될 수 없다

 

실제로는 이런 검증 실패 응답이 디버깅에 도움이 된다. 어느 위치의 어떤 값이 문제인지 FastAPI가 비교적 구체적으로 알려주기 때문이다.


 

기본값 넣기

모든 값을 매번 클라이언트가 보내야 하는 것은 아니다.

기본값을 지정할 수도 있다.

from pydantic import BaseModel

class Item(BaseModel):
    name: str
    price: int
    is_active: bool = True

 

이제 클라이언트가 is_active를 보내지 않아도 된다.

{
  "name": "맥북 파우치",
  "price": 25000
}

 

FastAPI는 is_active 값을 자동으로 True로 채운다.

이런 기본값은 게시글의 공개 여부, 상품 활성화 여부, 알림 수신 여부처럼 기본 상태가 정해져 있는 필드에 자주 사용한다.


 

선택값 처리하기

값이 없을 수도 있는 필드는 None을 허용하도록 작성할 수 있다.

Python 3.10 이상에서는 아래처럼 쓴다.

from pydantic import BaseModel

class Item(BaseModel):
    name: str
    price: int
    description: str | None = None

 

Python 3.9 이하 버전까지 고려한다면 Optional을 사용한다.

from typing import Optional
from pydantic import BaseModel

class Item(BaseModel):
    name: str
    price: int
    description: Optional[str] = None

 

두 예시는 의미가 거의 같다.

다만 str | None = None은 두 부분으로 나눠서 보는 게 좋다.

코드 의미
`str \ None` 이 필드에는 문자열이 들어올 수도 있고, None이 들어올 수도 있다
= None 클라이언트가 이 필드를 아예 보내지 않으면 기본값으로 None을 넣는다

 

str | None값의 타입을 정하는 부분이고, = None필드를 생략했을 때의 기본값을 정하는 부분이다.

초보자가 여기서 자주 헷갈리는 코드가 있다.

description: str | None

 

이렇게만 쓰면 None은 허용하지만, 필드 자체는 요청에 포함되어야 하는 것으로 해석될 수 있다.

Pydantic v2에서는 “필드가 필수인지 아닌지”를 판단할 때 기본값 유무가 중요하다. 그래서 실제로 생략 가능한 필드라면 아래처럼 기본값까지 같이 지정하는 편이 안전하다.

description: str | None = None

 

이렇게 써야 “안 보내도 되는 선택 필드”라는 의미가 더 분명해진다.

참고: [Pydantic 공식 문서 - Migration Guide](https://pydantic.dev/docs/validation/latest/get-started/migration/)


 

Field로 더 구체적인 조건 넣기

타입만으로는 부족한 경우가 있다.

예를 들어 가격은 정수여야 할 뿐 아니라 0보다 커야 한다. 상품명은 너무 짧으면 안 된다.

이럴 때 Field를 사용할 수 있다.

from pydantic import BaseModel, Field

class Item(BaseModel):
    name: str = Field(min_length=2, max_length=50)
    price: int = Field(gt=0)
    description: str | None = Field(default=None, max_length=300)

 

각 조건은 다음처럼 읽으면 된다.

코드 의미
min_length=2 최소 2글자
max_length=50 최대 50글자
gt=0 0보다 커야 함
default=None 기본값은 None

 

FastAPI 공식 문서에서도 Pydantic의 Field를 사용해 모델 속성에 추가 검증과 메타데이터를 선언할 수 있다고 설명한다.

참고: [FastAPI 공식 문서 - Body Fields](https://fastapi.tiangolo.com/tutorial/body-fields/)

여기서 gt는 greater than, 즉 “보다 큼”이라는 뜻이다.

price: int = Field(gt=0)

 

이 코드는 price가 0보다 큰 정수여야 한다는 의미다.

따라서 아래 요청은 실패한다.

{
  "name": "가방",
  "price": 0
}

 

가격이 0보다 커야 하는데 0이 들어왔기 때문이다.


 

요청 모델과 응답 모델은 분리하는 게 좋다

초보 단계에서는 요청으로 받은 모델을 그대로 응답해도 된다.

@app.post("/items")
def create_item(item: Item):
    return item

 

하지만 실제 서비스에서는 요청 모델과 응답 모델을 분리하는 경우가 많다.

예를 들어 상품을 만들 때 클라이언트는 name, price만 보낸다. 그런데 응답에는 서버가 만든 id도 포함해야 한다.

from fastapi import FastAPI
from pydantic import BaseModel, Field

app = FastAPI()

class ItemCreate(BaseModel):
    name: str = Field(min_length=2, max_length=50)
    price: int = Field(gt=0)
    description: str | None = None

class ItemResponse(BaseModel):
    id: int
    name: str
    price: int
    description: str | None = None

@app.post("/items", response_model=ItemResponse)
def create_item(item: ItemCreate):
    return {
        "id": 1,
        "name": item.name,
        "price": item.price,
        "description": item.description
    }

 

여기서 ItemCreate는 요청용 모델이고, ItemResponse는 응답용 모델이다.

FastAPI의 response_model은 응답 데이터 문서화, 검증, 변환, 필터링 등에 사용된다. 공식 문서에서도 response_model을 통해 응답 데이터 구조를 선언할 수 있다고 설명한다.

참고: [FastAPI 공식 문서 - Response Model](https://fastapi.tiangolo.com/tutorial/response-model/)

 

참고로 FastAPI에서는 함수의 반환 타입 힌트로 응답 모델을 표현할 수도 있다. ```python @app.post("/items") def create_item(item: ItemCreate) -> ItemResponse: pass ` 위 코드는 반환 타입 힌트 문법을 보여주기 위한 간단한 예시다. 실제 코드에서는 pass 대신 데이터를 저장하거나, 응답으로 보낼 값을 만들어 return해야 한다. 다만 입문 단계에서는 response_model=ItemResponse처럼 명시적으로 적는 방식이 요청 모델과 응답 모델의 차이를 이해하기 더 쉽다.

 

여기서 중요한 점은 보안이다.

예를 들어 회원가입 API에서 요청 모델에 password가 있다고 해서 응답에도 그대로 비밀번호를 보내면 안 된다.

class UserCreate(BaseModel):
    email: str
    password: str

class UserResponse(BaseModel):
    id: int
    email: str

 

요청과 응답 모델을 분리하면 이런 실수를 줄일 수 있다.


 

자주 헷갈리는 부분

1. Pydantic 모델은 데이터베이스 모델이 아니다

Pydantic 모델은 요청과 응답 데이터의 구조를 검증하는 데 주로 사용한다.

데이터베이스 테이블을 정의하는 모델과는 다르다.

예를 들어 SQLAlchemy 모델은 DB 테이블과 연결되고, Pydantic 모델은 API 입출력 데이터와 연결된다.

처음에는 둘 다 “모델”이라고 부르기 때문에 헷갈릴 수 있다.

Pydantic 모델: API 요청/응답 데이터 검증
ORM 모델: 데이터베이스 테이블 매핑

 

둘은 목적이 다르다.


2. 타입 힌트만 쓴다고 자동 검증되는 것은 아니다

파이썬 함수에서 이렇게 쓴다고 해서 일반 파이썬 코드가 자동으로 타입을 막아주는 것은 아니다.

def create_item(price: int):
    return price

 

파이썬의 타입 힌트는 기본적으로 개발자와 도구를 위한 힌트에 가깝다.

하지만 FastAPI와 Pydantic을 함께 쓰면 이 타입 정보가 실제 요청 검증에 활용된다.

이 차이를 이해해야 한다.


3. GET 요청에는 보통 Body를 쓰지 않는다

FastAPI는 복잡한 경우 GET 요청에 Body를 받는 것도 지원하지만, 일반적인 API 설계에서는 POST, PUT, PATCH 요청에서 Body를 사용하는 흐름이 더 자연스럽다.

FastAPI 공식 문서도 데이터를 보낼 때는 보통 POST, PUT, DELETE, PATCH 등을 사용한다고 설명한다.

참고: [FastAPI 공식 문서 - Request Body](https://fastapi.tiangolo.com/tutorial/body/)

조회 조건이 단순하면 GET의 쿼리 파라미터를 쓰고, 생성이나 수정처럼 데이터 묶음을 보내야 하면 요청 본문을 쓰는 식으로 나누면 된다.


 

전체 예제 코드

아래 코드는 지금까지 내용을 합친 간단한 예제다.

from fastapi import FastAPI
from pydantic import BaseModel, Field

app = FastAPI()

class ItemCreate(BaseModel):
    name: str = Field(min_length=2, max_length=50)
    price: int = Field(gt=0)
    description: str | None = Field(default=None, max_length=300)
    is_active: bool = True

class ItemResponse(BaseModel):
    id: int
    name: str
    price: int
    description: str | None = None
    is_active: bool

@app.post("/items", response_model=ItemResponse)
def create_item(item: ItemCreate):
    return {
        "id": 1,
        "name": item.name,
        "price": item.price,
        "description": item.description,
        "is_active": item.is_active
    }

 

서버 실행은 이전 글에서 사용한 방식과 동일하다.

fastapi dev main.py

 

파일 이름을 app.py로 만들었다면 아래처럼 파일명에 맞춰 실행하면 된다.

fastapi dev app.py

 

또는 환경에 따라 Uvicorn을 직접 실행할 수도 있다.

uvicorn main:app --reload

 

여기서 main:appmain.py 파일 안의 app = FastAPI() 객체를 실행한다는 뜻이다. 파일 이름이 app.py라면 uvicorn app:app --reload처럼 앞부분도 파일명에 맞춰 바꿔야 한다.

브라우저에서 아래 주소로 접속한다.

http://127.0.0.1:8000/docs

 

POST /items를 열고 Try it out을 누른 뒤 아래 JSON을 입력해보면 된다.

{
  "name": "맥북 파우치",
  "price": 25000,
  "description": "노트북 보호용 파우치",
  "is_active": true
}

 

이제 price0으로 바꾸거나, name을 한 글자로 줄여보면 검증 에러가 어떻게 나오는지도 확인할 수 있다.

실제로는 정상 요청보다 실패 요청을 일부러 보내보는 게 더 도움이 된다. Pydantic 모델이 어떤 기준으로 요청을 막는지 눈으로 볼 수 있기 때문이다.


 

FastAPI에서 Pydantic 모델을 배울 때의 기준

FastAPI에서 Pydantic 모델은 “예쁘게 클래스를 만드는 문법”이 아니다.

API가 받을 수 있는 데이터의 모양을 정하고, 잘못된 요청을 함수 실행 전에 걸러내고, Swagger UI 문서까지 자동으로 정리해주는 기준점에 가깝다.

처음에는 아래 네 가지만 구분하면 충분하다.

BaseModel: 데이터 구조 정의
타입 힌트: 필드 타입 지정
Field: 세부 검증 조건 추가
response_model: 응답 구조 지정

 

이 흐름이 잡히면 FastAPI의 POST, PUT, PATCH 요청을 훨씬 안정적으로 다룰 수 있다.

다음 단계에서는 중첩 모델, 리스트 요청, 여러 모델을 함께 받는 방식까지 확장하면 된다.