Data Validation Skill
Expert data validation for Python backends (Pydantic) and TypeScript frontends (Zod) with type-safe validation pipelines.
Quick Reference
| Layer | Tool | Purpose |
|---|---|---|
| Backend | Pydantic v2 | Request/response validation |
| Frontend | Zod | Form validation, schema inference |
| Sanitization | bleach, strip-html | Input cleaning |
| Types | mypy, TypeScript | Compile-time safety |
Project Structure
backend/
├── app/
│ ├── schemas/
│ │ ├── __init__.py
│ │ ├── student.py # StudentCreate, StudentUpdate, StudentOut
│ │ ├── fee.py # FeeCreate, FeeOut
│ │ └── common.py # PaginationParams, ErrorDetail
│ └── models/ # SQLModel (separate from schemas)
frontend/
├── lib/
│ ├── schemas/
│ │ ├── student.schema.ts
│ │ └── index.ts # Export all schemas
│ └── validation.ts # Shared validation utilities
└── types/
└── shared.ts # Inferred types from Zod
Backend: Pydantic Models
Separate In/Out Models
# backend/app/schemas/student.py
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, EmailStr, Field, ConfigDict
# === CREATE Models (Input) ===
class StudentCreate(BaseModel):
"""Payload for creating a new student."""
first_name: str = Field(
...,
min_length=1,
max_length=100,
examples=["John"],
description="Student's first name",
)
last_name: str = Field(
...,
min_length=1,
max_length=100,
examples=["Doe"],
)
email: EmailStr = Field(..., examples=["john.doe@school.edu"])
phone: Optional[str] = Field(
None,
pattern=r"^\+?[1-9]\d{9,14}$",
examples=["+1234567890"],
)
date_of_birth: datetime = Field(..., description="Student's birth date")
grade_level: int = Field(..., ge=1, le=12, examples=[9])
enrollment_date: datetime = Field(default_factory=datetime.utcnow)
model_config = ConfigDict(
json_schema_extra={
"examples": [
{
"first_name": "John",
"last_name": "Doe",
"email": "john.doe@school.edu",
"date_of_birth": "2008-05-15T00:00:00Z",
"grade_level": 9,
}
]
}
)
class StudentUpdate(BaseModel):
"""Payload for updating a student. All fields optional."""
first_name: Optional[str] = Field(None, min_length=1, max_length=100)
last_name: Optional[str] = Field(None, min_length=1, max_length=100)
email: Optional[EmailStr] = None
phone: Optional[str] = Field(None, pattern=r"^\+?[1-9]\d{9,14}$")
grade_level: Optional[int] = Field(None, ge=1, le=12)
is_active: Optional[bool] = None
# === OUTPUT Models (Response) ===
class StudentOut(BaseModel):
"""Response model for student data."""
id: int
first_name: str
last_name: str
email: str
phone: Optional[str] = None
date_of_birth: datetime
grade_level: int
enrollment_date: datetime
is_active: bool
created_at: datetime
updated_at: datetime
model_config = ConfigDict(from_attributes=True)
class StudentWithFees(StudentOut):
"""Student with nested fee information."""
fees: list["FeeOut"] = []
Custom Validators
# backend/app/schemas/student.py (continued)
from pydantic import field_validator, model_validator
from datetime import date
class StudentCreate(BaseModel):
# ... fields as above
@field_validator("date_of_birth", mode="before")
@classmethod
def parse_date_of_birth(cls, v):
if isinstance(v, str):
return datetime.fromisoformat(v.replace("Z", "+00:00"))
return v
@model_validator(mode="after")
def validate_age_range(self):
"""Ensure student is a reasonable age for school."""
dob = self.date_of_birth
if isinstance(dob, datetime):
dob = dob.date()
today = date.today()
age = today.year - dob.year - ((today.month, today.day) < (dob.month, dob.day))
if age < 5:
raise ValueError("Student must be at least 5 years old")
if age > 25:
raise ValueError("Age seems unreasonable for school enrollment")
return self
Common Schemas
# backend/app/schemas/common.py
from typing import Generic, TypeVar, Optional, List
from pydantic import BaseModel, Field
T = TypeVar("T")
class PaginationParams(BaseModel):
"""Standard pagination parameters."""
skip: int = Field(0, ge=0, description="Number of records to skip")
limit: int = Field(100, ge=1, le=1000, description="Max records to return")
class PaginatedResponse(BaseModel, Generic[T]):
"""Standard paginated response wrapper."""
data: List[T]
total: int = Field(..., description="Total count of records")
skip: int
limit: int
has_more: bool = Field(..., description="Whether more records exist")
class ErrorDetail(BaseModel):
"""Detailed validation error."""
field: str = Field(..., examples=["email"])
message: str = Field(..., examples=["Invalid email format"])
code: str = Field(..., examples=["value_error"])
class ValidationErrorResponse(BaseModel):
"""Standard validation error response."""
error: dict = Field(..., description="Error details")
details: Optional[List[ErrorDetail]] = None
# === Search & Filter ===
class SearchParams(BaseModel):
"""Standard search parameters."""
query: Optional[str] = Field(None, min_length=1, max_length=200)
sort_by: str = Field("created_at")
sort_order: str = Field("desc", pattern="^(asc|desc)$")
Frontend: Zod Schemas
Basic Schemas
// frontend/lib/schemas/student.schema.ts
import { z } from "zod";
// === CONSTANTS (shared with backend) ===
const NAME_MIN_LENGTH = 1;
const NAME_MAX_LENGTH = 100;
const PHONE_REGEX = /^\+?[1-9]\d{9,14}$/;
const GRADE_MIN = 1;
const GRADE_MAX = 12;
// === CREATE SCHEMA ===
export const studentCreateSchema = z.object({
first_name: z
.string()
.min(NAME_MIN_LENGTH, { message: "First name is required" })
.max(NAME_MAX_LENGTH, { message: "Maximum 100 characters allowed" })
.trim(),
last_name: z
.string()
.min(NAME_MIN_LENGTH, { message: "Last name is required" })
.max(NAME_MAX_LENGTH, { message: "Maximum 100 characters allowed" })
.trim(),
email: z.string().email({ message: "Invalid email address" }),
phone: z
.string()
.regex(PHONE_REGEX, { message: "Invalid phone number format" })
.optional()
.or(z.literal("")),
date_of_birth: z.string().datetime({ message: "Invalid date format" }),
grade_level: z
.number()
.int()
.min(GRADE_MIN, { message: `Grade must be at least ${GRADE_MIN}` })
.max(GRADE_MAX, { message: `Grade cannot exceed ${GRADE_MAX}` }),
});
// === UPDATE SCHEMA (partial) ===
export const studentUpdateSchema = studentCreateSchema.partial();
// === OUTPUT/RESPONSE SCHEMA ===
export const studentOutSchema = z.object({
id: z.number(),
first_name: z.string(),
last_name: z.string(),
email: z.string().email(),
phone: z.string().nullable(),
date_of_birth: z.string().datetime(),
grade_level: z.number().int().min(GRADE_MIN).max(GRADE_MAX),
enrollment_date: z.string().datetime(),
is_active: z.boolean(),
created_at: z.string().datetime(),
updated_at: z.string().datetime(),
});
// === INFERRED TYPES ===
export type StudentCreateInput = z.infer<typeof studentCreateSchema>;
export type StudentUpdateInput = z.infer<typeof studentUpdateSchema>;
export type StudentOutput = z.infer<typeof studentOutSchema>;
Advanced Zod Patterns
// frontend/lib/schemas/student.schema.ts (continued)
import { date, string } from "zod";
// === CROSS-FIELD VALIDATION ===
export const studentCreateSchema = z
.obje