
FastAPI ResponseValidationError는 API 함수가 반환한 값이 선언한 응답 모델과 맞지 않을 때 발생합니다. 요청 입력 검증이 아니라, 서버가 클라이언트에게 내보내려는 응답을 검증하는 단계에서 터지는 오류입니다.
핵심은 코드가 정상 실행됐는지와 응답 스키마가 맞는지는 다른 문제라는 점입니다. DB 조회는 성공했어도 response_model과 반환 객체의 모양이 다르면 응답 검증은 실패할 수 있습니다.

FastAPI ResponseValidationError와 response_model
FastAPI의 response_model은 단순한 문서용 타입 힌트가 아닙니다. FastAPI 문서는 response_model을 통해 출력 데이터를 검증하고, OpenAPI 문서를 만들고, 필요한 필드만 응답으로 내보낼 수 있다고 설명합니다.
from pydantic import BaseModel
from fastapi import FastAPI
app = FastAPI()
class UserOut(BaseModel):
id: int
name: str
@app.get("/users/{user_id}", response_model=UserOut)
def get_user(user_id: int):
return {"id": user_id, "username": "kim"}위 코드는 username을 반환하지만 response_model은 name을 기대합니다. 이런 불일치가 응답 검증 오류의 전형적인 출발점입니다.
가장 흔한 원인 1: 필드 이름이 다르다
DB 모델에서는 username이라고 부르는데 API 응답 모델에서는 name이라고 부르는 식의 차이가 있으면 검증이 실패합니다. dict를 반환하든 ORM 객체를 반환하든, 최종적으로 Pydantic 모델이 읽을 수 있는 필드가 있어야 합니다.
- response_model에 있는 필드가 반환값에 없다
- 반환값에는 있지만 필드 이름이 다르다
- alias를 쓰지만 populate 설정이 맞지 않는다
- 중첩 모델의 내부 필드가 비어 있다
가장 흔한 원인 2: None을 허용하지 않았다
DB에서는 nullable인 컬럼인데 응답 모델에서는 str처럼 필수 문자열로 선언한 경우도 자주 터집니다. 데이터에는 None이 들어올 수 있는데 모델은 None을 허용하지 않기 때문입니다.
class UserOut(BaseModel):
id: int
nickname: str | None = None
# DB nickname 컬럼이 nullable이면 Optional로 맞춰야 한다.반대로 정말 필수 필드라면 모델을 바꾸는 것이 아니라 DB 데이터와 비즈니스 규칙을 먼저 고쳐야 합니다. 오류를 없애려고 모든 필드를 Optional로 만드는 것은 좋은 해결책이 아닙니다.
가장 흔한 원인 3: ORM 객체를 그대로 반환했다
ORM 객체를 그대로 반환할 때는 Pydantic이 객체 속성을 읽어 응답 모델을 만들 수 있어야 합니다. Pydantic v2에서는 객체 속성 기반 검증이 from_attributes 설정과 연결됩니다.
from pydantic import BaseModel, ConfigDict
class UserOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
name: str다만 from_attributes는 만능 해결책이 아닙니다. 속성 이름이 다르거나, lazy relationship이 아직 로드되지 않았거나, async 환경에서 잘못된 시점에 속성 접근이 일어나면 다른 문제가 이어질 수 있습니다.
response serialization 시점을 봐야 한다
FastAPI 엔드포인트 함수가 return을 실행한 뒤에도 작업은 끝나지 않습니다. FastAPI는 반환값을 response_model에 맞춰 직렬화합니다. 이때 ORM 객체의 속성에 접근하거나, 중첩 모델을 펼치거나, 타입을 변환합니다.
따라서 오류 로그에서 실제 비즈니스 로직보다 응답 직렬화 단계가 중요할 때가 많습니다. 특히 SQLAlchemy async와 lazy loading이 엮이면 MissingGreenlet 같은 오류로 이어질 수 있습니다.
디버깅 순서
- 에러 메시지에서 어떤 필드가 실패했는지 확인한다
- response_model과 실제 반환값의 필드 이름을 비교한다
- None이 들어올 수 있는 필드인지 확인한다
- ORM 객체라면 from_attributes와 relationship loading을 확인한다
- 중첩 응답은 작은 모델부터 검증한다
나쁜 해결책: response_model을 지우기
ResponseValidationError가 불편하다고 response_model을 지우면 당장은 오류가 사라질 수 있습니다. 하지만 API 응답 계약도 같이 사라집니다. 클라이언트가 어떤 필드를 받을 수 있는지, 서버가 어떤 데이터를 내보내야 하는지 흐려집니다.
더 좋은 해결책은 response_model을 실제 응답 계약으로 유지하고, 반환 데이터를 그 계약에 맞추는 것입니다.
작은 재현 코드로 줄이기
실무 오류는 DB, dependency, middleware가 섞여 복잡해 보입니다. 먼저 작은 dict 반환 예제로 response_model 불일치를 재현해 보면 문제 위치가 빨리 보입니다.
class ItemOut(BaseModel):
id: int
title: str
price: int
@app.get("/items/{item_id}", response_model=ItemOut)
def get_item(item_id: int):
return {"id": item_id, "title": "keyboard", "price": None}위 예시는 price가 int인데 None을 반환합니다. DB나 ORM이 없어도 응답 검증이 왜 실패하는지 확인할 수 있습니다.
정리
FastAPI ResponseValidationError는 서버 내부 로직이 실패했다는 뜻만은 아닙니다. 서버가 반환한 데이터가 response_model이라는 출력 계약을 만족하지 못했다는 뜻입니다.
먼저 필드 이름, None 허용 여부, ORM 객체 속성 접근, 중첩 모델을 순서대로 확인하세요. 공식 기준은 FastAPI response_model 문서에서 확인할 수 있습니다. async ORM과 엮이는 문제는 MissingGreenlet 글과 함께 보면 좋습니다.