Pydantic Models & Naming Conventions
Quick reference for Pydantic model patterns, naming conventions and common usage in the starter app. This is just a suggestion.
tip
For comprehensive documentation, see the Pydantic documentation.
Naming convention
Standard naming pattern used in the starter app:
# 1. Base Model (shared fields to avoid duplications between DB models and API models aka DTO)
class TaskBase(ModelWithIdAndTimestamps):
"""Base model with core fields - no table=True"""
title: str = Field(max_length=200, description="Task title")
description: str | None = Field(default=None, max_length=1000)
completed: bool = Field(default=False)
priority: str = Field(default="medium")
# 2. Database Table Model (name without suffix)
class Task(TaskBase, table=True):
"""Database table model - inherits from Base + adds relationships"""
# Foreign keys
user_id: UUID = Field(foreign_key="user.id", index=True)
category_id: UUID | None = Field(foreign_key="category.id", default=None)
# Relationships
user: User = Relationship(back_populates="tasks")
category: Category | None = Relationship(back_populates="tasks")
# 3. API Models: auto-generated from Base with utility functions (see generated docs on `http://localhost:8000/docs`)
CreateTaskDTO = create_create_model(TaskBase, "CreateTask")
UpdateTaskDTO = create_update_model(TaskBase, "UpdateTask")
# 4. API Models: custom
class TaskDTO(BaseDTO, TaskBase):
"""API response model - includes id, timestamps + fields from TaskBase"""
pass
# 5. Extended Response Models (with relationships)
class TaskWithUserDTO(TaskDTO):
"""Task with user information"""
user: UserDTO | None = None
class TaskWithDetailsDTO(TaskDTO):
"""Task with all related data"""
user: UserDTO | None = None
category: CategoryDTO | None = None
tags: List[TagDTO] = []
# Instead of `DTO`, you can also use `Request`/`Response`
Field validation
Common validation patterns:
from pydantic import Field, validator, root_validator
from typing import Optional
import re
class UserBase(ModelWithIdAndTimestamps):
# Basic validation
email: str = Field(
...,
regex=r'^[^@]+@[^@]+\.[^@]+$',
description="Valid email address"
)
# String constraints
first_name: str = Field(min_length=1, max_length=50)
last_name: str = Field(min_length=1, max_length=50)
# Numeric constraints
age: int = Field(ge=0, le=150, description="Age in years")
# Enum-like validation
role: str = Field(regex="^(user|admin|moderator)$", default="user")
# Optional with default
bio: Optional[str] = Field(default=None, max_length=500)
# Custom validation
phone: Optional[str] = Field(default=None, regex=r'^\+?1?\d{9,15}$')
@validator('email')
def email_must_be_lowercase(cls, v):
"""Custom email validation"""
return v.lower()
@validator('first_name', 'last_name')
def names_must_not_be_empty(cls, v):
"""Ensure names aren't just whitespace"""
if not v or not v.strip():
raise ValueError('Name cannot be empty')
return v.strip().title()
@root_validator
def validate_user_data(cls, values):
"""Cross-field validation"""
first_name = values.get('first_name')
last_name = values.get('last_name')
email = values.get('email')
if first_name and last_name and email:
# Ensure email matches name pattern for corporate users
if email.endswith('@company.com'):
expected = f"{first_name.lower()}.{last_name.lower()}@company.com"
if email != expected:
raise ValueError(f'Corporate email must be {expected}')
return values