Pydantic v2 DTO/스키마 설계 패턴. Use when: 요청/응답 DTO, CamelModel, dto/ 폴더 구조, camelCase 변환, alias_generator, model_config, 검증 로직, field_validator, model_validator, partial update (apply_simple_fields), 페이지네이션 응답, 에러 응답 (ErrorBody), from_domain 팩토리, Pydantic BaseModel 설정, JSON 직렬화. NOT for: 도메인 엔티티 (domain-layer), DB 모델 (sqlalchemy).
Install
npx skillscat add nomik94/claude-code-preset/pydantic-schema Install via the SkillsCat registry.
SKILL.md
Pydantic v2 DTO 패턴
CamelModel (Base DTO)
모든 DTO가 상속하는 기반 클래스. 이름은 반드시 CamelModel.
from pydantic import BaseModel, ConfigDict
from pydantic.alias_generators import to_camel
class CamelModel(BaseModel):
model_config = ConfigDict(
from_attributes=True,
populate_by_name=True,
alias_generator=to_camel,
)- MUST:
CamelModel명칭 사용 (BaseSchema 금지) - MUST: 모든 DTO는 CamelModel 상속
- MUST:
from_attributes=True(ORM 변환용)
dto/ 폴더 구조
dto/는 처음부터 폴더로 생성. endpoint 1:1 파일 매핑.
src/{domain}/dto/
__init__.py # re-export all DTOs
create_user.py # CreateUserRequest, CreateUserResponse
update_user.py # UpdateUserRequest
list_users.py # ListUsersRequest (query params), UserListItem
get_user.py # UserDetailResponse
common.py # shared nested schemas (AddressResponse, etc.)- MUST: endpoint당 1개 파일 (create_user.py, list_users.py ...)
- MUST: 단일 dto.py 금지 -- 반드시 폴더 구조
- MUST:
__init__.py에서 re-export
Request/Response 패턴
# dto/create_user.py
class CreateUserRequest(CamelModel):
email: str
name: str
password: str
password_confirm: str
# dto/update_user.py -- partial update
class UpdateUserRequest(CamelModel):
email: str | None = None
name: str | None = None
phone: str | None = None
# dto/get_user.py
class UserDetailResponse(CamelModel):
id: int
email: str
name: str
role: UserRole
is_active: bool
created_at: datetimeapply_simple_fields() -- Partial Update
PATCH 엔드포인트에서 model_fields_set을 수동 순회하지 않고 헬퍼 메서드 사용.
class UpdateUserRequest(CamelModel):
email: str | None = None
name: str | None = None
phone: str | None = None
def apply_simple_fields(self, entity: UserEntity) -> None:
for field_name in self.model_fields_set:
setattr(entity, field_name, getattr(self, field_name))- MUST: partial update 시
apply_simple_fields()패턴 사용 - MUST:
model_fields_set직접 순회 코드를 서비스 레이어에 노출하지 않음 - 복잡한 필드(비밀번호 해싱 등)는
apply_simple_fields()외부에서 별도 처리
페이지네이션
T = TypeVar("T", bound=BaseModel)
class PaginatedResponse(CamelModel, Generic[T]):
items: Sequence[T]
total: int
page: int
size: int
@computed_field # type: ignore[prop-decorator]
@property
def total_pages(self) -> int:
return (self.total + self.size - 1) // self.size if self.size > 0 else 0
@computed_field # type: ignore[prop-decorator]
@property
def has_next(self) -> bool:
return self.page < self.total_pagesUsage: PaginatedResponse[UserListItem]
ErrorBody
통일된 에러 응답 구조. 이름은 반드시 ErrorBody + FieldError.
class FieldError(CamelModel):
field: str
message: str
class ErrorBody(CamelModel):
code: str # "VALIDATION_ERROR", "NOT_FOUND", "CONFLICT"
message: str # human-readable summary
errors: list[FieldError] = []- MUST:
ErrorBody명칭 사용 (ErrorResponse, ErrorDetail 금지) - MUST: 필드 에러는
FieldError(field + message) - MUST:
errors필드명 사용 (details 금지) - 422 override 시
errors에 각 필드별 FieldError 매핑
Validators
class CreateUserRequest(CamelModel):
email: str
password: str
password_confirm: str
@field_validator("email")
@classmethod
def validate_email_format(cls, v: str) -> str:
if not re.match(r"^[\w.+-]+@[\w-]+\.[\w.]+$", v):
raise ValueError("Invalid email format")
return v.lower().strip()
@model_validator(mode="after")
def validate_passwords_match(self) -> Self:
if self.password != self.password_confirm:
raise ValueError("Passwords do not match")
return self- MUST:
field_validator/model_validator사용 (@validator금지) - MUST:
model_validator(mode="after")반환 타입은Self
Enum 통합
class UserRole(StrEnum):
ADMIN = "admin"
MEMBER = "member"
GUEST = "guest"
class UserDetailResponse(CamelModel):
role: UserRole # auto-validates, serializes to stringfrom_domain 팩토리
ORM from_attributes 대신 도메인 엔티티에서 직접 변환할 때 사용.
class UserDetailResponse(CamelModel):
id: int
email: str
name: str
@classmethod
def from_domain(cls, entity: "UserEntity") -> Self:
return cls(
id=entity.id,
email=entity.email.value, # unwrap value objects
name=entity.name,
)- MUST: 반환 타입
Self(-> "ClassName"금지) - from_domain은 Response DTO에만 정의
직렬화 Quick Reference
| Operation | Code |
|---|---|
| ORM -> DTO | UserDetailResponse.model_validate(db_user) |
| Entity -> DTO | UserDetailResponse.from_domain(entity) |
| DTO -> dict (camel) | dto.model_dump(by_alias=True, exclude_none=True) |
| DTO -> JSON string | dto.model_dump_json(by_alias=True) |
| Batch convert | [UserListItem.model_validate(u) for u in db_users] |
Checklist
- CamelModel을 base로 사용 (BaseSchema 아님)
- dto/ 폴더 구조, endpoint 1:1 파일
-
__init__.py에서 모든 DTO re-export - Partial update는
apply_simple_fields()패턴 - 에러 응답은 ErrorBody + FieldError
- Python 3.13+ 문법:
X | None,list[X],StrEnum - Pydantic v2:
model_config,model_dump(),model_validate() - from_domain 반환 타입
Self - 도메인 엔티티에 Pydantic import 없음