coredot.today
Pydantic 기초 — 데이터의 모양을 선언하고 검증하기 (FastAPI 입문 4편)
블로그로 돌아가기
PydanticFastAPIBaseModelFieldValidationError타입 변환strictfield_validatormodel_validatormodel_dumpaliasEmailStr입문튜토리얼

Pydantic 기초 — 데이터의 모양을 선언하고 검증하기 (FastAPI 입문 4편)

FastAPI 코드의 절반은 Pydantic이다. 이 편은 FastAPI를 잠시 내려놓고 Pydantic만 떼어 파이썬 코드로 익힌다. 모델을 만들고, 틀린 값에서 나오는 ValidationError를 읽고, 필수·선택·null의 네 가지 조합, 기본(lax) 모드의 타입 변환 규칙과 strict 모드, Field 제약, 중첩 모델, field_validator·model_validator, model_dump·model_validate, camelCase 별칭까지 — 전부 실제 실행 결과로 보여 준다. 필드 타입별로 값이 어떻게 바뀌는지 lax와 strict를 나란히 비교하는 위젯을 포함한다.

코어닷투데이2026-09-2746분

들어가며 — 낯선 줄의 정체

Pydantic 기초크게 보기

3편의 예제 코드에는 이런 줄이 있었다.

python
class TaskCreate(BaseModel):
    title: str = Field(min_length=1, max_length=100, description="할 일 제목", examples=["보고서 초안 쓰기"])
    priority: int = Field(default=3, ge=1, le=5, description="1(낮음) ~ 5(높음)", examples=[4])

/docs의 Schemas 칸, Try it out의 예시 값, 틀린 요청에 돌아오던 422 에러 — 전부 이 몇 줄에서 나왔다. 이것이 Pydantic이다. FastAPI는 요청을 받고 응답을 보내는 뼈대이고, 데이터를 검사하고 변환하는 일은 거의 전부 Pydantic이 한다. 그래서 FastAPI를 “Starlette + Pydantic”이라고 부르기도 한다(1편 5절).

다음 편부터 CRUD를 만들며 Pydantic 모델을 잔뜩 쓴다. 그 전에 이 편에서는 FastAPI 없이 Pydantic만 떼어 파이썬 코드로 익힌다. 서버도 브라우저도 필요 없다. 파이썬 파일 하나나 대화형 셸(python3)이면 충분하다. fastapi[standard]를 설치했다면 Pydantic도 이미 들어 있다.

1. Pydantic이 푸는 문제 — 밖에서 온 데이터는 믿을 수 없다

설계도대로 검사해 객체로 만든다크게 보기

API 서버에 들어오는 JSON은 파이썬에서 그냥 딕셔너리다. 딕셔너리는 아무것도 약속하지 않는다. 키가 빠질 수도, 숫자 자리에 글자가 올 수도, 음수 나이가 올 수도 있다. 이걸 직접 검사하면 이렇게 된다.

python
def check_user(data: dict) -> dict:
    if "name" not in data or not isinstance(data["name"], str):
        raise ValueError("name은 문자열이어야 합니다")
    if "age" not in data:
        raise ValueError("age가 없습니다")
    try:
        age = int(data["age"])
    except (TypeError, ValueError):
        raise ValueError("age는 정수여야 합니다")
    if age < 0:
        raise ValueError("age는 0 이상이어야 합니다")
    email = data.get("email")
    if email is not None and not isinstance(email, str):
        raise ValueError("email은 문자열이어야 합니다")
    return {"name": data["name"], "age": age, "email": email}

필드 세 개에 열다섯 줄이다. 게다가 첫 번째 에러에서 멈춰서, 두 곳이 틀려도 하나만 알려 준다. 필드가 스무 개인 API가 서른 개라면? Pydantic으로 같은 일을 하면 이렇다.

python
from pydantic import BaseModel, Field


class User(BaseModel):
    name: str
    age: int = Field(ge=0)
    email: str | None = None

데이터의 모양을 클래스로 선언하면, 검사·변환·에러 메시지는 Pydantic이 만든다. 이 클래스를 모델이라고 부른다. 설계도라고 생각하면 쉽다. 설계도(모델)를 주면 Pydantic이 들어온 재료(딕셔너리)를 검사해서, 맞으면 반듯한 객체로 만들어 주고 틀리면 무엇이 왜 틀렸는지 전부 적어 돌려준다.

2. 첫 모델 만들기

python
from pydantic import BaseModel


class User(BaseModel):
    name: str
    age: int
    email: str | None = None


u = User(name="김철수", age="30")
print(repr(u))
print(u.name, u.age, type(u.age).__name__)

실제 출력이다.

text
User(name='김철수', age=30, email=None)
김철수 30 int

세 가지를 확인할 수 있다.

  1. BaseModel을 상속하고, 필드를 이름: 타입으로 적는다. 파이썬 타입 힌트 문법 그대로다.
  2. age="30"(문자열)을 넣었는데 30(정수)이 됐다. Pydantic은 기본적으로 “뜻이 분명하면 바꿔 준다”. 이 규칙은 5절에서 자세히 본다.
  3. email을 안 넣었더니 None이 됐다. 기본값을 준 필드는 생략할 수 있다.

만든 객체는 평범한 파이썬 객체처럼 u.name, u.age로 쓴다. 에디터가 필드 이름을 자동완성해 주고, u.nmae 같은 오타는 에디터와 타입 검사기가 잡아 준다. 딕셔너리의 data["nmae"]는 실행해 봐야 안다.

3. 틀리면 — ValidationError 읽기

python
from pydantic import ValidationError

try:
    User(name="김철수", age="서른")
except ValidationError as e:
    print(e)
text
1 validation error for User
age
  Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='서른', input_type=str]
    For further information visit https://errors.pydantic.dev/2.13/v/int_parsing

틀린 값이 들어오면 Pydantic은 객체를 만들지 않고 ValidationError 를 던진다. 읽는 법은 위에서 아래로.

몇 개·어느 모델
1 validation error for User — User 모델에서 에러 1개.
어느 필드
age
무엇이 왜
Input should be a valid integer..., 종류는 type=int_parsing, 들어온 값은 '서른'.
더 알아보기
에러 종류마다 공식 설명 페이지 주소가 붙는다.

e.errors()를 부르면 같은 내용이 리스트로 나온다. 원소 하나하나에 type, loc, msg, input이 들어 있다. 어디서 본 모양 아닌가? 2편에서 읽는 법을 외운 FastAPI의 422 응답 detail이 바로 이 리스트다. FastAPI는 Pydantic의 ValidationError를 받아서 위치(loc) 앞에 "body"·"query"·"path"를 붙여 JSON으로 돌려줄 뿐이다. Pydantic 에러를 읽을 줄 알면 FastAPI 에러도 읽을 수 있다.

4. 필수·선택·null — 네 가지 조합

처음 배울 때 가장 헷갈리는 부분이다. 필드는 두 가지 질문으로 나뉜다. “안 보내도 되나?”(기본값이 있나)와 “null(None)이어도 되나?”(타입에 | None이 있나). 둘은 서로 다른 질문이다.

선언안 보내면None을 보내면언제 쓰나
name: str에러 (missing)에러 (string_type)반드시 있어야 하는 값 — 제목, 이메일
name: str = "무명"기본값 "무명"에러생략하면 정해진 값 — 우선순위 3
name: str | None = NoneNoneNone없어도 되는 값 — 메모, 설명
name: str | None에러 (missing)None드물다 — “비어 있음”을 명시적으로 보내게 할 때

마지막 줄이 함정이다. | None을 붙이면 “선택”이 된다고 착각하기 쉽지만, 기본값이 없으면 여전히 필수다. 직접 확인해 보자.

python
class Memo(BaseModel):
    text: str | None          # 기본값 없음


Memo()            # ValidationError — type: missing
Memo(text=None)   # 통과 — text=None
💡
외우는 법: = 기본값은 “안 보내도 된다”, | None은 “비어 있어도 된다”. 선택 필드를 만들 때는 거의 항상 둘을 같이 쓴다 — str | None = None. 6편의 PATCH 모델이 전부 이 모양이고, 그 편에서 이 두 질문이 섞일 때 생기는 버그(PATCH로 null이 들어가는 문제)를 다룬다.

5. 타입 변환 규칙 — lax 모드와 strict 모드

2절에서 "30"이 30이 됐다. Pydantic의 기본 모드는 lax(너그러운) 모드다. “이 값을 이 타입으로 바꿔도 뜻이 분명한가?”를 보고 분명하면 바꿔 준다. 그런데 그 경계가 직관과 다를 때가 있다. 아래 위젯에서 필드 타입을 골라 JSON 값 15가지가 어떻게 되는지 보자. 모든 결과는 실제 Pydantic을 돌린 값이다.

위젯에서 꼭 확인할 것 다섯 가지:

타입받아 준다 (lax)거절한다놀라운 점
int"3", 3.03.5(깎지 않음), "abc"true가 1이 된다
str문자열만숫자 3도 거절숫자 쪽과 반대로 엄격. 빈 문자열 ""은 통과
booltrue, "yes", "on", "1"3, "abc"문자열 “yes”가 True
datetimeISO 문자열, 날짜만(자정으로)"abc""3"이 1970-01-01 00:00:03 — 숫자를 유닉스 시간으로 읽는다
list[int][1, "2"] → [1, 2]"1,2"원소도 하나씩 변환한다

lax 모드는 편하다. 특히 경로·쿼리 매개변수는 원래 전부 문자열로 오기 때문에(2편), 변환이 없으면 /users/42조차 쓸 수 없다. 하지만 “true가 1이 된다”처럼 원치 않는 변환이 문제라면 strict 모드를 켠다.

python
from pydantic import BaseModel, ConfigDict


class Payment(BaseModel):
    model_config = ConfigDict(strict=True)   # 이 모델 전체를 엄격하게
    amount: int


Payment(amount="30")   # ValidationError — type: int_type, "Input should be a valid integer"
Payment(amount=30)     # 통과

필드 하나만 엄격하게 하려면 amount: int = Field(strict=True)로 쓴다. 금액·수량처럼 잘못 바뀌면 사고가 나는 값에만 거는 것이 보통이다. 요청 본문 전체를 strict로 바꾸면 “숫자를 문자열로 보내는” 구버전 클라이언트가 한꺼번에 깨질 수 있으니 팀에서 정하고 쓰자.

6. Field — 규칙과 설명 더하기

타입만으로 부족한 규칙은 Field(...)로 더한다. 2편의 Query(ge=1, le=100)과 같은 이름을 쓴다.

인자대상뜻예
default모두기본값 (없으면 필수)Field(default=3)
min_length / max_length문자열·리스트길이 범위Field(min_length=1, max_length=100)
pattern문자열정규식에 맞아야 함Field(pattern=r"^\d{3}-\d{4}$")
ge / gt숫자이상(≥) / 초과(>)Field(ge=0)
le / lt숫자이하(≤) / 미만(<)Field(le=5)
multiple_of숫자배수Field(multiple_of=100) — 100원 단위
description / examples모두문서용 설명과 예시 — 검사에는 영향 없음3편의 /docs에 그대로 나온다
alias모두JSON에서 쓰는 다른 이름11절

회원 가입 예시다.

python
from pydantic import BaseModel, EmailStr, Field


class Signup(BaseModel):
    email: EmailStr
    nickname: str = Field(min_length=2, max_length=10, pattern=r"^[가-힣a-zA-Z0-9]+$")


Signup(email="a@b.com", nickname="코어닷")        # 통과
Signup(email="not-email", nickname="코어 닷!")    # 에러 2개

두 번째 줄의 실제 에러(요약):

loctypemsg
emailvalue_errorvalue is not a valid email address: An email address must have an @-sign.
nicknamestring_pattern_mismatchString should match pattern '^[가-힣a-zA-Z0-9]+$'

틀린 곳 두 개가 한 번에 나왔다. 1절에서 직접 쓴 검사 함수는 첫 번째에서 멈췄다. 폼 화면이라면 이 차이가 크다 — 사용자가 “제출 → 에러 → 고침 → 제출 → 또 에러”를 반복하지 않는다.

7. 자주 쓰는 타입들

타입받는 값틀리면 (type)
str, int, float, bool기본 값들 (변환 규칙은 5절)string_type, int_parsing …
date, datetime"2026-09-27", "2026-09-27T10:30:00"date_parsing, datetime_parsing
Literal["blog", "news"]나열한 값 중 하나literal_error
Enum 클래스Enum의 값 중 하나 (2편의 Category)enum
EmailStr이메일 형식 문자열 (email-validator 필요 — fastapi[standard]에 포함)value_error
HttpUrl"https://core.today/blog" — "core.today"처럼 스킴이 없으면 거절url_parsing
UUIDUUID 문자열uuid_parsing
list[X], dict[str, X]배열·객체 (원소도 X로 검사)list_type + 원소의 에러
다른 BaseModel중첩 객체 (8절)안쪽 필드의 에러

8. 모델 안에 모델 — 중첩

실제 데이터는 대개 겹겹이다. 주문 하나에 상품이 여러 개 들어 있다. 모델의 필드 타입으로 다른 모델을 쓰면 된다.

python
class Item(BaseModel):
    name: str
    price: int = Field(ge=0)


class Order(BaseModel):
    order_id: int
    items: list[Item]
    memo: str | None = None


order = Order(order_id=2, items=[{"name": "사과", "price": 1000}])
print(order.items[0].name, type(order.items[0]).__name__)   # 사과 Item

딕셔너리로 넣었는데 order.items[0]은 Item 객체가 됐다. 안쪽까지 전부 검사하고 변환한다. 두 번째 상품의 가격이 음수면 에러의 위치가 이렇게 나온다(실제 출력).

python
Order.model_validate({"order_id": 1, "items": [{"name": "사과", "price": 1000}, {"name": "배", "price": -500}]})
# loc: ('items', 1, 'price'), type: greater_than_equal

('items', 1, 'price') — “items의 1번(두 번째) 원소의 price”. 위치가 경로처럼 정확하게 찍히니, 프론트엔드는 상품 목록의 두 번째 줄 가격 칸에 빨간 표시를 할 수 있다.

9. 직접 검사 규칙 만들기 — validator

Field로 표현할 수 없는 규칙은 함수로 쓴다. 두 종류가 있다.

9-1. field_validator — 필드 하나를 검사하거나 다듬기

python
from pydantic import BaseModel, field_validator


class TaskCreate(BaseModel):
    title: str

    @field_validator("title")
    @classmethod
    def strip_title(cls, value: str) -> str:
        value = value.strip()              # 앞뒤 공백 제거
        if not value:
            raise ValueError("제목이 비어 있습니다")
        return value                       # 돌려준 값이 최종 값이 된다
  • @field_validator("필드 이름") + @classmethod를 붙인 함수. 순서는 이대로 외우자.
  • 돌려준 값이 필드의 최종 값이 된다. 검사뿐 아니라 “다듬기”(공백 제거, 소문자로 바꾸기)에도 쓴다.
  • 틀렸으면 ValueError를 던진다. 에러 메시지는 "Value error, 제목이 비어 있습니다"처럼 들어간다.
  • 타입 변환이 끝난 뒤 실행된다. 그래서 value는 이미 str이다.
  • 값이 실제로 들어왔을 때만 실행된다. 안 보낸 필드의 기본값에는 돌지 않는다. 6편에서 이 성질을 이용해 PATCH의 null 함정을 막는다.

9-2. model_validator — 여러 필드를 함께 검사하기

“종료일은 시작일보다 빠를 수 없다”처럼 두 필드를 비교해야 하는 규칙은 모델 전체를 보는 검사기를 쓴다.

python
from datetime import date

from pydantic import BaseModel, model_validator


class Period(BaseModel):
    start: date
    end: date

    @model_validator(mode="after")
    def check_order(self):
        if self.end < self.start:
            raise ValueError("end는 start보다 빠를 수 없습니다")
        return self


Period(start="2026-10-01", end="2026-09-01")
# ValidationError — type: value_error, msg: "Value error, end는 start보다 빠를 수 없습니다"

mode="after"는 “필드 검사가 모두 끝난 뒤에” 실행하라는 뜻이다. 그래서 self.start와 self.end는 이미 date 객체이고 바로 비교할 수 있다.

10. 꺼내고 넣기 — model_dump와 model_validate

모델은 들어올 때(딕셔너리·JSON → 객체)와 나갈 때(객체 → 딕셔너리·JSON) 모두 쓴다.

메서드방향실제 결과 (2절의 u)
User.model_validate(dict)딕셔너리 → 객체User.model_validate({"name": "이영희", "age": 25}) → name='이영희' age=25 email=None
User.model_validate_json(str)JSON 문자열 → 객체파싱과 검사를 한 번에 — 가장 빠르다
u.model_dump()객체 → 딕셔너리{'name': '김철수', 'age': 30, 'email': None}
u.model_dump_json()객체 → JSON 문자열{"name":"김철수","age":30,"email":null} — 한글도 그대로
u.model_copy(update={...})일부만 바꾼 복사본u.model_copy(update={"age": 31}) → age만 31, 원본 u는 30 그대로

model_dump()에는 자주 쓰는 옵션이 있다.

  • exclude_unset=True — 실제로 들어온 필드만 꺼낸다. 6편 PATCH의 핵심.
  • exclude_none=True — 값이 None인 필드를 뺀다.
  • mode="json" — datetime을 문자열로 바꾸는 등 JSON에 넣을 수 있는 값으로 꺼낸다.
  • by_alias=True — 11절의 별칭 이름으로 꺼낸다.
⚠️
검사는 “만들 때”만 한다. 이미 만든 객체의 필드에 나중에 값을 넣으면(u.age = "abc") 기본 설정에서는 검사하지 않고 그대로 들어간다(실제로 'abc'가 들어갔다). model_copy(update=...)도 마찬가지다. 대입할 때도 검사하려면 모델 설정(model_config)에서 validate_assignment=True를 켠다 — 그러면 같은 대입이 ValidationError를 낸다. 6편에서 이 성질 때문에 생기는 버그를 직접 본다.

11. 파이썬은 snake_case, 프론트엔드는 camelCase

파이썬은 created_at, 자바스크립트는 createdAt을 쓴다. 한쪽이 양보해야 하는데, Pydantic의 별칭(alias) 을 쓰면 코드는 파이썬답게 두고 JSON만 camelCase로 주고받을 수 있다.

python
from datetime import datetime

from pydantic import BaseModel, ConfigDict
from pydantic.alias_generators import to_camel


class TaskOut(BaseModel):
    model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)

    task_id: int
    created_at: datetime


t = TaskOut.model_validate({"taskId": 1, "createdAt": "2026-09-27T10:00:00"})
t.model_dump()                             # {'task_id': 1, 'created_at': datetime(...)}
t.model_dump(by_alias=True, mode="json")   # {'taskId': 1, 'createdAt': '2026-09-27T10:00:00'}
  • alias_generator=to_camel — 모든 필드에 camelCase 별칭을 자동으로 붙인다.
  • populate_by_name=True — 파이썬 코드에서는 원래 이름(TaskOut(task_id=2, ...))으로도 만들 수 있게 한다.
  • FastAPI에서 이 모델을 response_model로 쓰면 응답이 자동으로 별칭 이름({"taskId": 1, "createdAt": ...})으로 나간다(실제 확인).

어느 쪽으로 통일할지는 7편의 팀 규칙 표에서 정한다. 중요한 건 API마다 섞이지 않게 하는 것이다.

12. 알아 두면 편한 성질 두 가지

① 리스트 기본값을 그냥 써도 된다. 일반 파이썬 함수에서 def f(tags=[])는 모든 호출이 같은 리스트를 공유하는 유명한 함정이다. Pydantic 모델은 객체마다 새로 복사한다.

python
class Box(BaseModel):
    tags: list[str] = []


a, b = Box(), Box()
a.tags.append("x")
print(a.tags, b.tags)   # ['x'] []  — b는 영향 없음

② 모델은 스스로를 설명한다. User.model_json_schema()를 부르면 모델이 JSON Schema로 나온다(실제 출력에서 발췌).

json
{
  "properties": {
    "name": {"description": "이름", "minLength": 1, "title": "Name", "type": "string"},
    "age": {"minimum": 0, "title": "Age", "type": "integer"},
    "email": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Email"}
  },
  "required": ["name", "age"],
  "title": "User",
  "type": "object"
}

required에 email이 없는 것, Field(ge=0)이 "minimum": 0이 된 것을 보자. FastAPI가 3편의 /openapi.json과 /docs의 Schemas 칸을 만드는 재료가 이것이다. 모델을 고치면 문서가 따라 바뀌는 이유다.

13. FastAPI에서는 어디에 쓰이나

이제 다음 편부터 쓸 곳을 미리 정리하자. 모두 같은 BaseModel이다.

쓰는 곳코드 모양Pydantic이 하는 일나오는 편
요청 본문def create(payload: TaskCreate)JSON 본문 검사·변환, 틀리면 4225편
응답@app.get(..., response_model=Task)나가는 데이터를 모양대로 거르기 (비밀번호 등 숨김)5편
부분 수정payload.model_dump(exclude_unset=True)보낸 필드만 꺼내기6편
쿼리 묶음def list(f: Annotated[FilterParams, Query()])쿼리 매개변수 여러 개를 모델 하나로 받아 검사—
설정class Settings(BaseSettings)환경 변수·.env를 읽어 타입 검사7편

쿼리 묶음은 이 시리즈 본편에서 쓰지 않지만 알아 두면 좋다. 목록 API의 필터가 많아지면 이렇게 모델 하나로 모을 수 있다.

python
from typing import Annotated

from fastapi import FastAPI, Query
from pydantic import BaseModel, Field

app = FastAPI()


class FilterParams(BaseModel):
    done: bool | None = None
    limit: int = Field(20, ge=1, le=100)
    q: str | None = None


@app.get("/tasks")
def list_tasks(filters: Annotated[FilterParams, Query()]):
    return filters

/tasks?limit=5&q=a는 {"done": null, "limit": 5, "q": "a"}가 되고, /tasks?limit=500은 422다. /docs에는 done·limit·q가 각각의 쿼리 매개변수로 나온다(실제 확인).

14. 연습 문제

파이썬 파일 하나로 풀 수 있다. FastAPI는 필요 없다.

  1. 상품 모델 Product를 만들자. name(1~50자, 필수), price(0 이상 정수, 필수), tags(문자열 리스트, 기본 빈 리스트), discount_rate(0 이상 1 이하 소수, 선택).
  2. Product(name="", price="1,000")을 만들면 에러가 몇 개, 어떤 type으로 나오는가? "1000"이면?
  3. sale_price와 price가 모두 있을 때 sale_price가 price보다 크면 에러를 내는 model_validator를 추가해 보자.
예시 답안
python
from pydantic import BaseModel, Field, model_validator


class Product(BaseModel):
    name: str = Field(min_length=1, max_length=50)
    price: int = Field(ge=0)
    tags: list[str] = []
    discount_rate: float | None = Field(default=None, ge=0, le=1)
    sale_price: int | None = Field(default=None, ge=0)

    @model_validator(mode="after")
    def check_sale_price(self):
        if self.sale_price is not None and self.sale_price > self.price:
            raise ValueError("할인가는 정가보다 클 수 없습니다")
        return self

2번: name은 string_too_short, price는 "1,000"을 정수로 못 바꿔 int_parsing — 에러 2개가 한 번에 나온다. "1000"이면 정수 1000으로 바뀌어 name 에러 하나만 남는다.

정리

📐
모델 = 데이터의 설계도
BaseModel에 이름: 타입을 적으면 검사·변환·에러 메시지가 따라온다. 틀리면 ValidationError — 그 errors()가 FastAPI 422의 detail이다.
🧭
두 질문을 구분하자
= 기본값은 “안 보내도 되나”, | None은 “비어 있어도 되나”. 기본 모드는 너그럽게 변환("3"→3, true→1)하고, 사고가 나면 안 되는 값에는 strict.
🧰
도구 상자
Field(범위·길이·패턴·설명), 중첩 모델, field_validator(필드 하나)·model_validator(여러 필드), model_dump/model_validate, 별칭(camelCase). 검사는 만들 때만 한다는 것도 기억하자.

이제 Pydantic 모델을 읽고 쓸 수 있다. 다음 편에서는 이 모델을 FastAPI에 꽂아 할 일 API의 만들기(POST)와 읽기(GET) 를 구현한다. 입력 모델과 출력 모델을 왜 나누는지, response_model이 무엇을 막아 주는지도 함께 본다.

ℹ️
실행 환경. 이 글의 모든 출력(ValidationError 문구, 네 가지 필수·선택 조합, lax/strict 변환표 15×8칸, EmailStr·pattern 에러, 중첩 모델의 loc, validator 결과, model_dump 계열, alias와 FastAPI 응답의 camelCase, 리스트 기본값 복사, 대입 시 미검증과 validate_assignment, JSON Schema, 쿼리 모델)은 Pydantic 2.13.5 · FastAPI 0.141.1 · Python 3.11에서 직접 실행한 결과다. 연습 문제 답안도 실행해 확인했다. 삽화는 코어닷투데이가 생성했다.