Alert

์ด ๊ธ€์€ Claude Code์˜ ๋„์›€์„ ๋ฐ›์•„ ์ž‘์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค

TL;DR

  • pydantic์€ Python์˜ ํƒ€์ž… ํžŒํŠธ๋ฅผ ๋Ÿฐํƒ€์ž„์— ๊ฐ•์ œํ•˜๋Š” ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
  • dataclass๋Š” ํƒ€์ž… ํžŒํŠธ๊ฐ€ ํžŒํŠธ์ผ ๋ฟ์ด์ง€๋งŒ, pydantic์€ ์‹ค์ œ๋กœ ๊ฒ€์ฆํ•˜๊ณ  ๋ณ€ํ™˜ํ•œ๋‹ค
  • FastAPI์˜ ์š”์ฒญ/์‘๋‹ต ์ฒ˜๋ฆฌ, ์„ค์ • ๊ด€๋ฆฌ, ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ ๋“ฑ์—์„œ ํ•ต์‹ฌ์ ์œผ๋กœ ์‚ฌ์šฉ๋œ๋‹ค

Sources


1. pydantic์ด๋ž€

pydantic์€ Python์˜ ํƒ€์ž… ํžŒํŠธ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ๊ณผ ์ž๋™ ๋ณ€ํ™˜์„ ํ•ด์ฃผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋‹ค. ํ•ต์‹ฌ ์ฐจ์ด๋ฅผ ์ฝ”๋“œ๋กœ ๋ณด๋ฉด ๋ฐ”๋กœ ์ดํ•ด๋œ๋‹ค.

dataclass โ€” ํƒ€์ž… ํžŒํŠธ๋Š” โ€œํžŒํŠธโ€์ผ ๋ฟ
from dataclasses import dataclass
 
@dataclass
class User:
    name: str
    age: int
 
u = User(name="alice", age="30")
print(u.age)        # "30" โ€” ๋ฌธ์ž์—ด ๊ทธ๋Œ€๋กœ
print(type(u.age))  # <class 'str'>

age: int๋ผ๊ณ  ์ ์—ˆ์ง€๋งŒ ๋ฌธ์ž์—ด "30"์ด ๊ทธ๋Œ€๋กœ ๋“ค์–ด๊ฐ„๋‹ค. ๋Ÿฐํƒ€์ž„์—์„œ ํƒ€์ž…์„ ํ™•์ธํ•˜์ง€ ์•Š๋Š”๋‹ค.

pydantic โ€” ํƒ€์ž…์„ โ€œ๊ฐ•์ œโ€ํ•œ๋‹ค
from pydantic import BaseModel
 
class User(BaseModel):
    name: str
    age: int
 
u = User(name="alice", age="30")
print(u.age)        # 30 โ€” int๋กœ ๋ณ€ํ™˜๋จ
print(type(u.age))  # <class 'int'>

๋ฌธ์ž์—ด "30"์„ ๋„ฃ์–ด๋„ int๋กœ ์ž๋™ ๋ณ€ํ™˜๋œ๋‹ค. ๋ณ€ํ™˜ ๋ถˆ๊ฐ€๋Šฅํ•˜๋ฉด ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.

User(name="alice", age="abc")
# ValidationError: 1 validation error for User
# age
#   Input should be a valid integer, unable to parse string as an integer

์„ค์น˜

pydantic์€ ์„œ๋“œํŒŒํ‹ฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋‹ค. ๋ณ„๋„ ์„ค์น˜๊ฐ€ ํ•„์š”ํ•˜๋‹ค.

pip install pydantic

2. ๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ•

๋ชจ๋ธ ์ •์˜

BaseModel์„ ์ƒ์†ํ•˜๊ณ  ํ•„๋“œ๋ฅผ ํƒ€์ž… ์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ ์„ ์–ธํ•œ๋‹ค.

from pydantic import BaseModel
 
class Product(BaseModel):
    name: str
    price: float
    quantity: int = 0        # ๊ธฐ๋ณธ๊ฐ’
    tags: list[str] = []     # mutable ๊ธฐ๋ณธ๊ฐ’๋„ ์•ˆ์ „ (dataclass์™€ ๋‹ฌ๋ฆฌ field() ๋ถˆํ•„์š”)
p = Product(name="ํ‚ค๋ณด๋“œ", price="89000")  # ๋ฌธ์ž์—ด โ†’ float ์ž๋™ ๋ณ€ํ™˜
print(p)
# name='ํ‚ค๋ณด๋“œ' price=89000.0 quantity=0 tags=[]
์ž๋™ ๋ณ€ํ™˜ ๊ทœ์น™

pydantic์€ ๊ฐ€๋Šฅํ•œ ํ•œ ์„ ์–ธ๋œ ํƒ€์ž…์œผ๋กœ ๋ณ€ํ™˜์„ ์‹œ๋„ํ•œ๋‹ค.

class Example(BaseModel):
    a: int
    b: float
    c: str
    d: bool
 
e = Example(a="42", b="3.14", c=123, d="yes")
print(e)
# a=42 b=3.14 c='123' d=True
์ž…๋ ฅ์„ ์–ธ ํƒ€์ž…๊ฒฐ๊ณผ
"42"int42
"3.14"float3.14
123str"123"
"yes"boolTrue
"abc"intValidationError
ValidationError

๊ฒ€์ฆ ์‹คํŒจ ์‹œ ValidationError๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. ์–ด๋–ค ํ•„๋“œ์—์„œ ์–ด๋–ค ์ด์œ ๋กœ ์‹คํŒจํ–ˆ๋Š”์ง€ ์ƒ์„ธ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•œ๋‹ค.

from pydantic import ValidationError
 
class User(BaseModel):
    name: str
    age: int
    email: str
 
try:
    User(name=123, age="abc", email="alice@example.com")
except ValidationError as e:
    print(e.error_count())  # 1 โ€” age๋งŒ ์‹คํŒจ (name์€ "123"์œผ๋กœ ๋ณ€ํ™˜ ๊ฐ€๋Šฅ)
    print(e.errors())
    # [{'type': 'int_parsing', 'loc': ('age',), 'msg': 'Input should be a valid integer...'}]

3. Field()

Field()๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๊ธฐ๋ณธ๊ฐ’, ๋ณ„์นญ, ๊ฒ€์ฆ ์ œ์•ฝ ์กฐ๊ฑด ๋“ฑ์„ ํ•„๋“œ ๋‹จ์œ„๋กœ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.

๊ธฐ๋ณธ ์‚ฌ์šฉ
from pydantic import BaseModel, Field
 
class User(BaseModel):
    name: str = Field(min_length=1, max_length=50, description="์‚ฌ์šฉ์ž ์ด๋ฆ„")
    age: int = Field(ge=0, le=150, description="๋‚˜์ด")
    email: str = Field(pattern=r"^[\w.-]+@[\w.-]+\.\w+$")
User(name="", age=30, email="alice@example.com")
# ValidationError โ€” name์ด ๋นˆ ๋ฌธ์ž์—ด (min_length=1 ์œ„๋ฐ˜)
 
User(name="alice", age=-1, email="alice@example.com")
# ValidationError โ€” age๊ฐ€ ์Œ์ˆ˜ (ge=0 ์œ„๋ฐ˜)
 
User(name="alice", age=30, email="not-an-email")
# ValidationError โ€” pattern ๋ถˆ์ผ์น˜
Field() ์ฃผ์š” ์˜ต์…˜
์˜ต์…˜์šฉ๋„์˜ˆ์‹œ
default๊ธฐ๋ณธ๊ฐ’Field(default=0)
default_factorymutable ๊ธฐ๋ณธ๊ฐ’Field(default_factory=list)
aliasJSON ํ‚ค ์ด๋ฆ„ ๋งคํ•‘Field(alias="userName")
min_length / max_length๋ฌธ์ž์—ด ๊ธธ์ด ์ œํ•œField(min_length=1)
ge / gt / le / lt์ˆซ์ž ๋ฒ”์œ„Field(ge=0, le=100)
pattern์ •๊ทœ์‹ ๊ฒ€์ฆField(pattern=r"^\d{3}-\d{4}$")
descriptionJSON Schema ์„ค๋ช…Field(description="์‚ฌ์šฉ์ž ID")
exclude์ง๋ ฌํ™” ์‹œ ์ œ์™ธField(exclude=True)
alias โ€” ์™ธ๋ถ€ JSON ํ‚ค์™€ Python ํ•„๋“œ๋ช… ๋งคํ•‘

์™ธ๋ถ€ API์˜ JSON ํ‚ค๊ฐ€ Python ๋„ค์ด๋ฐ ์ปจ๋ฒค์…˜๊ณผ ๋‹ค๋ฅผ ๋•Œ ์œ ์šฉํ•˜๋‹ค.

class User(BaseModel):
    user_name: str = Field(alias="userName")
    created_at: str = Field(alias="createdAt")
 
# JSON์—์„œ๋Š” camelCase๋กœ ๋ฐ›๊ณ 
data = {"userName": "alice", "createdAt": "2026-04-12"}
u = User(**data)
 
# Python์—์„œ๋Š” snake_case๋กœ ์ ‘๊ทผ
print(u.user_name)   # alice
print(u.created_at)  # 2026-04-12

4. ๊ฒ€์ฆ์ž (Validator)

Field()์˜ ์ œ์•ฝ ์กฐ๊ฑด๋งŒ์œผ๋กœ ๋ถ€์กฑํ•  ๋•Œ, ์ปค์Šคํ…€ ๊ฒ€์ฆ ๋กœ์ง์„ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋กœ ์ •์˜ํ•  ์ˆ˜ ์žˆ๋‹ค.

@field_validator โ€” ํ•„๋“œ ๋‹จ์œ„ ๊ฒ€์ฆ
from pydantic import BaseModel, field_validator
 
class User(BaseModel):
    username: str
    password: str
 
    @field_validator("username")
    @classmethod
    def username_must_be_alphanumeric(cls, v: str) -> str:
        if not v.isalnum():
            raise ValueError("์˜๋ฌธ๊ณผ ์ˆซ์ž๋งŒ ํ—ˆ์šฉ๋ฉ๋‹ˆ๋‹ค")
        return v
 
    @field_validator("password")
    @classmethod
    def password_must_be_strong(cls, v: str) -> str:
        if len(v) < 8:
            raise ValueError("๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” 8์ž ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค")
        if not any(c.isupper() for c in v):
            raise ValueError("๋Œ€๋ฌธ์ž๊ฐ€ ์ตœ์†Œ 1๊ฐœ ํฌํ•จ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค")
        return v
User(username="alice123", password="MyPass123")  # OK
 
User(username="alice!@#", password="MyPass123")
# ValidationError โ€” ์˜๋ฌธ๊ณผ ์ˆซ์ž๋งŒ ํ—ˆ์šฉ๋ฉ๋‹ˆ๋‹ค
 
User(username="alice", password="short")
# ValidationError โ€” ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” 8์ž ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค
@field_validator์˜ ๊ฐ’ ๋ณ€ํ™˜

validator์—์„œ ๊ฐ’์„ ๋ณ€ํ™˜ํ•ด์„œ ๋ฐ˜ํ™˜ํ•  ์ˆ˜๋„ ์žˆ๋‹ค. ๊ฒ€์ฆ๊ณผ ์ •๊ทœํ™”๋ฅผ ๋™์‹œ์— ์ฒ˜๋ฆฌํ•œ๋‹ค.

class Tag(BaseModel):
    name: str
 
    @field_validator("name")
    @classmethod
    def normalize(cls, v: str) -> str:
        return v.strip().lower()
 
print(Tag(name="  Python  "))  # name='python'
@model_validator โ€” ๋ชจ๋ธ ๋‹จ์œ„ ๊ฒ€์ฆ (์—ฌ๋Ÿฌ ํ•„๋“œ ์กฐํ•ฉ)

ํ•„๋“œ ๊ฐ„ ๊ด€๊ณ„๋ฅผ ๊ฒ€์ฆํ•  ๋•Œ ์‚ฌ์šฉํ•œ๋‹ค.

from pydantic import BaseModel, model_validator
 
class DateRange(BaseModel):
    start: str
    end: str
 
    @model_validator(mode="after")
    def check_date_order(self):
        if self.start >= self.end:
            raise ValueError("start๋Š” end๋ณด๋‹ค ์ด์ „์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค")
        return self
DateRange(start="2026-01-01", end="2026-12-31")  # OK
DateRange(start="2026-12-31", end="2026-01-01")   # ValidationError
mode=โ€œbeforeโ€ vs mode=โ€œafterโ€
class User(BaseModel):
    name: str
    age: int
 
    # before: ํƒ€์ž… ๋ณ€ํ™˜ ์ „์— ์‹คํ–‰ (raw ์ž…๋ ฅ๊ฐ’์„ ๋ฐ›์Œ)
    @model_validator(mode="before")
    @classmethod
    def preprocess(cls, data):
        if isinstance(data, dict) and "full_name" in data:
            data["name"] = data.pop("full_name")
        return data
 
    # after: ํƒ€์ž… ๋ณ€ํ™˜ ํ›„์— ์‹คํ–‰ (๊ฒ€์ฆ๋œ ๋ชจ๋ธ ์ธ์Šคํ„ด์Šค๋ฅผ ๋ฐ›์Œ)
    @model_validator(mode="after")
    def postprocess(self):
        self.name = self.name.title()
        return self
u = User(**{"full_name": "alice", "age": 30})
print(u.name)  # Alice โ€” before์—์„œ ํ‚ค ๋ณ€ํ™˜, after์—์„œ title() ์ ์šฉ

5. ์ง๋ ฌํ™”/์—ญ์ง๋ ฌํ™”

pydantic์€ ์ง๋ ฌํ™” ๋ฉ”์„œ๋“œ๋ฅผ ๋‚ด์žฅํ•˜๊ณ  ์žˆ๋‹ค. asdict + json.dumps ์กฐํ•ฉ์ด ํ•„์š”ํ•œ dataclass์™€ ๋‹ฌ๋ฆฌ ํ•œ ์ค„๋กœ ์ฒ˜๋ฆฌ๋œ๋‹ค.

Python ๊ฐ์ฒด โ†’ dict / JSON
class Address(BaseModel):
    city: str
    zipcode: str
 
class User(BaseModel):
    name: str
    age: int
    address: Address
 
u = User(name="alice", age=30, address=Address(city="์„œ์šธ", zipcode="06000"))
 
# dict๋กœ ๋ณ€ํ™˜
print(u.model_dump())
# {'name': 'alice', 'age': 30, 'address': {'city': '์„œ์šธ', 'zipcode': '06000'}}
 
# JSON ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜
print(u.model_dump_json(indent=2))
# {
#   "name": "alice",
#   "age": 30,
#   "address": {
#     "city": "์„œ์šธ",
#     "zipcode": "06000"
#   }
# }
dict / JSON โ†’ Python ๊ฐ์ฒด
# dict์—์„œ ์ƒ์„ฑ
data = {"name": "bob", "age": 25, "address": {"city": "๋ถ€์‚ฐ", "zipcode": "48000"}}
u = User.model_validate(data)
 
# JSON ๋ฌธ์ž์—ด์—์„œ ์ƒ์„ฑ
json_str = '{"name": "bob", "age": 25, "address": {"city": "๋ถ€์‚ฐ", "zipcode": "48000"}}'
u = User.model_validate_json(json_str)

์ค‘์ฒฉ๋œ ๋ชจ๋ธ๋„ ์ž๋™์œผ๋กœ ํŒŒ์‹ฑ๋œ๋‹ค. dict ์•ˆ์˜ {"city": "๋ถ€์‚ฐ", "zipcode": "48000"}์ด Address ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜๋œ๋‹ค.

์ง๋ ฌํ™” ์˜ต์…˜
class User(BaseModel):
    name: str
    password: str = Field(exclude=True)  # ์ง๋ ฌํ™” ์‹œ ์ œ์™ธ
    age: int
 
u = User(name="alice", password="secret123", age=30)
print(u.model_dump())
# {'name': 'alice', 'age': 30} โ€” password ์ œ์™ธ๋จ
# ํŠน์ • ํ•„๋“œ๋งŒ ํฌํ•จ/์ œ์™ธ
u.model_dump(include={"name", "age"})   # {'name': 'alice', 'age': 30}
u.model_dump(exclude={"age"})           # {'name': 'alice'}
dataclass์™€ ๋น„๊ต
์ž‘์—…dataclasspydantic
dict ๋ณ€ํ™˜asdict(obj)obj.model_dump()
JSON ๋ณ€ํ™˜json.dumps(asdict(obj))obj.model_dump_json()
dict โ†’ ๊ฐ์ฒด์ˆ˜๋™ ๋งคํ•‘ ํ•„์š”Model.model_validate(dict)
JSON โ†’ ๊ฐ์ฒดjson.loads() + ์ˆ˜๋™ ๋งคํ•‘Model.model_validate_json(str)
์ค‘์ฒฉ ๊ฐ์ฒด ํŒŒ์‹ฑ์ˆ˜๋™์ž๋™

6. ๋ชจ๋ธ ์„ค์ • (model_config)

model_config๋ฅผ ํ†ตํ•ด ๋ชจ๋ธ ์ „์ฒด์˜ ๋™์ž‘์„ ์ œ์–ดํ•  ์ˆ˜ ์žˆ๋‹ค.

from pydantic import ConfigDict
strict โ€” ์ž๋™ ๋ณ€ํ™˜ ๋„๊ธฐ

๊ธฐ๋ณธ์ ์œผ๋กœ pydantic์€ "30" โ†’ 30์ฒ˜๋Ÿผ ๋ณ€ํ™˜์„ ์‹œ๋„ํ•œ๋‹ค. strict=True๋กœ ์„ค์ •ํ•˜๋ฉด ์ •ํ™•ํ•œ ํƒ€์ž…๋งŒ ํ—ˆ์šฉํ•œ๋‹ค.

class StrictUser(BaseModel):
    model_config = ConfigDict(strict=True)
 
    name: str
    age: int
 
StrictUser(name="alice", age=30)    # OK
StrictUser(name="alice", age="30")  # ValidationError โ€” str์€ int๊ฐ€ ์•„๋‹˜
frozen โ€” ๋ถˆ๋ณ€ ๋ชจ๋ธ

dataclass์˜ frozen=True์™€ ๋™์ผํ•˜๋‹ค.

class Config(BaseModel):
    model_config = ConfigDict(frozen=True)
 
    host: str
    port: int
 
c = Config(host="localhost", port=8080)
c.host = "0.0.0.0"  # ValidationError โ€” frozen์ด๋ผ ๋ณ€๊ฒฝ ๋ถˆ๊ฐ€
extra โ€” ์ •์˜ํ•˜์ง€ ์•Š์€ ํ•„๋“œ ์ฒ˜๋ฆฌ
# forbid: ์ •์˜ํ•˜์ง€ ์•Š์€ ํ•„๋“œ๊ฐ€ ์˜ค๋ฉด ์—๋Ÿฌ (๊ธฐ๋ณธ๊ฐ’์€ ignore)
class StrictModel(BaseModel):
    model_config = ConfigDict(extra="forbid")
    name: str
 
StrictModel(name="alice", unknown_field="?")
# ValidationError โ€” extra inputs are not permitted
# allow: ์ •์˜ํ•˜์ง€ ์•Š์€ ํ•„๋“œ๋„ ์ €์žฅ
class FlexModel(BaseModel):
    model_config = ConfigDict(extra="allow")
    name: str
 
m = FlexModel(name="alice", custom="value")
print(m.custom)  # value
populate_by_name โ€” alias์™€ ํ•„๋“œ๋ช… ๋‘˜ ๋‹ค ํ—ˆ์šฉ
class User(BaseModel):
    model_config = ConfigDict(populate_by_name=True)
 
    user_name: str = Field(alias="userName")
 
# ๋‘˜ ๋‹ค ๊ฐ€๋Šฅ
User(userName="alice")    # OK (alias)
User(user_name="alice")   # OK (ํ•„๋“œ๋ช…)

7. ์ค‘์ฒฉ ๋ชจ๋ธ๊ณผ JSON Schema

์ค‘์ฒฉ ๋ชจ๋ธ ์ž๋™ ํŒŒ์‹ฑ

pydantic์˜ ๊ฐ€์žฅ ๊ฐ•๋ ฅํ•œ ๊ธฐ๋Šฅ ์ค‘ ํ•˜๋‚˜๋‹ค. JSON์˜ ์ค‘์ฒฉ ๊ตฌ์กฐ๋ฅผ ๋ชจ๋ธ ์ •์˜๋งŒ์œผ๋กœ ์ž๋™ ํŒŒ์‹ฑํ•œ๋‹ค.

class Address(BaseModel):
    city: str
    zipcode: str
 
class Company(BaseModel):
    name: str
    address: Address
 
class User(BaseModel):
    name: str
    age: int
    company: Company
    hobbies: list[str] = []
# ๊นŠ๊ฒŒ ์ค‘์ฒฉ๋œ dict๋ฅผ ๋„ฃ์–ด๋„ ์ž๋™์œผ๋กœ ๊ฐ ๋ชจ๋ธ๋กœ ๋ณ€ํ™˜๋œ๋‹ค
data = {
    "name": "alice",
    "age": 30,
    "company": {
        "name": "Acme",
        "address": {
            "city": "์„œ์šธ",
            "zipcode": "06000"
        }
    },
    "hobbies": ["python", "coffee"]
}
 
u = User(**data)
print(type(u.company))          # <class 'Company'>
print(type(u.company.address))  # <class 'Address'>
print(u.company.address.city)   # ์„œ์šธ

dataclass์—์„œ ๊ฐ™์€ ์ผ์„ ํ•˜๋ ค๋ฉด ์ค‘์ฒฉ๋œ dict๋ฅผ ์ˆ˜๋™์œผ๋กœ ๋งคํ•‘ํ•ด์•ผ ํ•œ๋‹ค.

JSON Schema ์ž๋™ ์ƒ์„ฑ

pydantic ๋ชจ๋ธ์—์„œ JSON Schema๋ฅผ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. FastAPI๋Š” ์ด ๊ธฐ๋Šฅ์„ ์ด์šฉํ•ด API ๋ฌธ์„œ(Swagger UI)๋ฅผ ์ž๋™ ์ƒ์„ฑํ•œ๋‹ค.

import json
 
schema = User.model_json_schema()
print(json.dumps(schema, indent=2))
# {
#   "properties": {
#     "name": { "type": "string", "title": "Name" },
#     "age": { "type": "integer", "title": "Age" },
#     "company": { "$ref": "#/$defs/Company" },
#     "hobbies": {
#       "items": { "type": "string" },
#       "type": "array",
#       "default": [],
#       "title": "Hobbies"
#     }
#   },
#   "required": ["name", "age", "company"],
#   ...
# }

8. FastAPI์™€์˜ ์—ฐ๋™

FastAPI๋Š” ๋‚ด๋ถ€์ ์œผ๋กœ pydantic์„ ์‚ฌ์šฉํ•œ๋‹ค. ์š”์ฒญ body๋ฅผ pydantic ๋ชจ๋ธ๋กœ ์„ ์–ธํ•˜๋ฉด ๊ฒ€์ฆ, ๋ณ€ํ™˜, ๋ฌธ์„œํ™”๊ฐ€ ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌ๋œ๋‹ค.

from fastapi import FastAPI
from pydantic import BaseModel, Field
 
app = FastAPI()
 
class CreateUserRequest(BaseModel):
    name: str = Field(min_length=1, max_length=50)
    age: int = Field(ge=0, le=150)
    email: str
 
class UserResponse(BaseModel):
    id: int
    name: str
    email: str
 
@app.post("/users", response_model=UserResponse)
async def create_user(req: CreateUserRequest):
    # req๋Š” ์ด๋ฏธ ๊ฒ€์ฆ/๋ณ€ํ™˜์ด ์™„๋ฃŒ๋œ pydantic ๋ชจ๋ธ
    # name์ด ๋นˆ ๋ฌธ์ž์—ด์ด๊ฑฐ๋‚˜ age๊ฐ€ ์Œ์ˆ˜๋ฉด ์—ฌ๊ธฐ๊นŒ์ง€ ์˜ค์ง€ ์•Š๋Š”๋‹ค (422 ์—๋Ÿฌ)
    return UserResponse(id=1, name=req.name, email=req.email)

์ด ์ฝ”๋“œ๋งŒ์œผ๋กœ ๋‹ค์Œ์ด ์ž๋™์œผ๋กœ ๋™์ž‘ํ•œ๋‹ค:

  • ์š”์ฒญ JSON โ†’ CreateUserRequest ๊ฒ€์ฆ + ๋ณ€ํ™˜
  • ๊ฒ€์ฆ ์‹คํŒจ ์‹œ 422 Unprocessable Entity ์‘๋‹ต (์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ํฌํ•จ)
  • ์‘๋‹ต์„ UserResponse ์Šคํ‚ค๋งˆ๋กœ ์ง๋ ฌํ™”
  • Swagger UI(/docs)์— ์š”์ฒญ/์‘๋‹ต ์Šคํ‚ค๋งˆ ์ž๋™ ํ‘œ์‹œ
Query Parameter ๊ฒ€์ฆ

body๋ฟ ์•„๋‹ˆ๋ผ query parameter์—๋„ pydantic ๊ฒ€์ฆ์„ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

from fastapi import Query
 
@app.get("/users")
async def list_users(
    page: int = Query(ge=1, default=1),
    size: int = Query(ge=1, le=100, default=20),
    sort: str = Query(pattern=r"^(name|age|created)$", default="name"),
):
    return {"page": page, "size": size, "sort": sort}

9. dataclass vs pydantic ์ •๋ฆฌ

๊ธฐ์ค€dataclasspydantic
์„ค์น˜๋ถˆํ•„์š” (ํ‘œ์ค€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ)pip install pydantic
ํƒ€์ž… ๊ฒ€์ฆX (ํžŒํŠธ๋งŒ)O (๋Ÿฐํƒ€์ž„ ๊ฒ€์ฆ + ์ž๋™ ๋ณ€ํ™˜)
๊ธฐ๋ณธ๊ฐ’ (mutable)field(default_factory=list)[] ๊ทธ๋Œ€๋กœ ๊ฐ€๋Šฅ
ํ•„๋“œ ์ œ์•ฝ ์กฐ๊ฑด__post_init__์—์„œ ์ง์ ‘ ๊ตฌํ˜„Field(ge=0, max_length=50)
์ปค์Šคํ…€ ๊ฒ€์ฆ__post_init__@field_validator, @model_validator
dict ๋ณ€ํ™˜asdict().model_dump()
JSON ๋ณ€ํ™˜asdict() + json.dumps().model_dump_json()
JSON โ†’ ๊ฐ์ฒด์ˆ˜๋™ ๋งคํ•‘.model_validate_json()
์ค‘์ฒฉ ๊ฐ์ฒด ํŒŒ์‹ฑ์ˆ˜๋™์ž๋™
JSON SchemaX.model_json_schema()
๋ถˆ๋ณ€ ๋ชจ๋ธfrozen=Truemodel_config = ConfigDict(frozen=True)
์„ฑ๋Šฅ๋น ๋ฆ„ (boilerplate ์ƒ์„ฑ์ผ ๋ฟ)v2์—์„œ ํฌ๊ฒŒ ๊ฐœ์„  (Rust ๊ธฐ๋ฐ˜ ์ฝ”์–ด)
FastAPI ์—ฐ๋™X๊ธฐ๋ณธ ํ†ตํ•ฉ
ํŒ๋‹จ ๊ธฐ์ค€
์™ธ๋ถ€ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์„œ ๊ฒ€์ฆํ•ด์•ผ ํ•˜๋Š”๊ฐ€?
โ”œโ”€โ”€ Yes โ†’ pydantic
โ”‚         (API ์ž…๋ ฅ, JSON ํŒŒ์‹ฑ, ํ™˜๊ฒฝ๋ณ€์ˆ˜, ์‚ฌ์šฉ์ž ์ž…๋ ฅ)
โ””โ”€โ”€ No โ†’ ๋‚ด๋ถ€ ๋ฐ์ดํ„ฐ๋ฅผ ๊ตฌ์กฐํ™”๋งŒ ํ•˜๋ฉด ๋˜๋Š”๊ฐ€?
          โ”œโ”€โ”€ Yes โ†’ dataclass (๊ฐ€๋ณ๊ณ  ํ‘œ์ค€)
          โ””โ”€โ”€ ๊ฒ€์ฆ๋„ ์•ฝ๊ฐ„ ํ•„์š” โ†’ pydantic ๋˜๋Š” attrs

๊ฐ™์ด ์“ฐ๋Š” ๊ฒƒ๋„ ๊ฐ€๋Šฅํ•˜๋‹ค

ํ•˜๋‚˜์˜ ํ”„๋กœ์ ํŠธ์—์„œ ๋‘˜์„ ์„ž์–ด ์“ฐ๋Š” ๊ฒƒ์€ ์ž์—ฐ์Šค๋Ÿฝ๋‹ค. API ๊ฒฝ๊ณ„(์ž…์ถœ๋ ฅ)์—์„œ๋Š” pydantic์œผ๋กœ ๊ฒ€์ฆํ•˜๊ณ , ๋‚ด๋ถ€ ๋กœ์ง์—์„œ๋Š” dataclass๋กœ ๊ฐ€๋ณ๊ฒŒ ๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌํ•˜๋Š” ํŒจํ„ด์ด ์ผ๋ฐ˜์ ์ด๋‹ค.