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
Consistent Naming
Following consistent naming patterns helps everyone understand the codebase quickly:
- Base: Shared fields between DB and API models (avoid duplication)
- No suffix: Database models (e.g.,
Task,User) - DTO/Request/Response suffix: API models for data transfer
- With- prefix: Models including relationships (e.g.,
TaskWithUserDTO)
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") # All fields required except those with defaults
UpdateTaskDTO = create_update_model(TaskBase, "UpdateTask") # All fields optional for partial updates
# 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
Validation Best Practices
- Validate early: Use Pydantic to catch errors before they hit the database
- Be specific: Use precise constraints (e.g.,
min_length,regex) rather than generic validation - Meaningful errors: Pydantic automatically generates clear error messages for API responses
- Reuse validators: Use the same validator for multiple fields with similar rules
For latest validation features, check Pydantic V2 validators.
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 - always store emails in lowercase"""
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