Create and modify Pydantic schemas (backend) and TypeScript types (frontend) while ensuring end-to-end type safety and CamelCase compliance. Use when creating response/request schemas, adding bilingual fields, generating TypeScript types from Pydantic schemas, creating Zod validation schemas, or ensuring API contracts match between frontend and backend.
This skill ensures type safety between the FastAPI backend and Next.js frontend. The key challenge is maintaining consistent API contracts while handling:
CRITICAL: All Pydantic schemas MUST inherit from
CamelModel. Failure breaks frontend parsing.
Activate when request involves:
name_en, name_ar)| Component | Path |
|---|---|
| Base Schema | src/backend/api/schemas/_base.py |
| User Schemas | src/backend/api/schemas/user_schemas.py |
| Role Schemas | src/backend/api/schemas/role_schemas.py |
| All Schemas | src/backend/api/schemas/*.py |
| Component | Path |
|---|---|
| Types Directory | src/my-app/types/ |
| User Types | src/my-app/types/user.ts |
| Role Types | src/my-app/types/role.ts |
| Scheduler Types | src/my-app/types/scheduler.ts |
| Zod Schemas | src/my-app/lib/validations/ |
# src/backend/api/schemas/_base.py is the source of truth
from api.schemas._base import CamelModel
from typing import Optional
from datetime import datetime
class UserResponse(CamelModel):
"""
Response schema for user data.
Field names are snake_case in Python, automatically converted to
camelCase in JSON responses.
"""
id: str # → "id"
username: str # → "username"
full_name: Optional[str] = None # → "fullName"
is_active: bool # → "isActive"
is_super_admin: bool = False # → "isSuperAdmin"
created_at: datetime # → "createdAt" (UTC with 'Z')
updated_at: Optional[datetime] = None # → "updatedAt"
class UserCreate(CamelModel):
"""Request schema for creating a user."""
username: str
full_name: Optional[str] = None
is_active: bool = True
role_ids: list[int] = [] # → "roleIds"
// src/my-app/types/user.ts
export interface User {
id: string;
username: string;
fullName: string | null;
isActive: boolean;
isSuperAdmin: boolean;
createdAt: string; // ISO datetime string
updatedAt: string | null;
}
export interface UserCreate {
username: string;
fullName?: string;
isActive?: boolean;
roleIds?: number[];
}
| Python (Backend) | JSON/TypeScript (Frontend) |
|---|---|
user_id | userId |
full_name | fullName |
is_active | isActive |
created_at | createdAt |
role_ids | roleIds |
name_en | nameEn |
name_ar | nameAr |
class RoleResponse(CamelModel):
"""Role with bilingual support."""
id: int
name_en: str # → "nameEn"
name_ar: str # → "nameAr"
description_en: Optional[str] = None # → "descriptionEn"
description_ar: Optional[str] = None # → "descriptionAr"
is_active: bool
class RoleCreate(CamelModel):
"""Create role with bilingual names."""
name_en: str
name_ar: str
description_en: Optional[str] = None
description_ar: Optional[str] = None
// src/my-app/types/role.ts
export interface Role {
id: number;
nameEn: string;
nameAr: string;
descriptionEn: string | null;
descriptionAr: string | null;
isActive: boolean;
}
export interface RoleCreate {
nameEn: string;
nameAr: string;
descriptionEn?: string;
descriptionAr?: string;
}
// Helper for displaying localized name
export function getLocalizedName(
item: { nameEn: string; nameAr: string },
locale: 'en' | 'ar'
): string {
return locale === 'ar' ? item.nameAr : item.nameEn;
}
// src/my-app/lib/validations/role.ts
import { z } from 'zod';
export const roleCreateSchema = z.object({
nameEn: z.string()
.min(1, 'English name is required')
.max(100, 'Name must be 100 characters or less'),
nameAr: z.string()
.min(1, 'Arabic name is required')
.max(100, 'Name must be 100 characters or less'),
descriptionEn: z.string().max(500).optional(),
descriptionAr: z.string().max(500).optional(),
});
export type RoleCreateInput = z.infer<typeof roleCreateSchema>;
// For forms with react-hook-form
export const roleUpdateSchema = roleCreateSchema.partial();
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { roleCreateSchema, RoleCreateInput } from '@/lib/validations/role';
function CreateRoleForm() {
const form = useForm<RoleCreateInput>({
resolver: zodResolver(roleCreateSchema),
defaultValues: {
nameEn: '',
nameAr: '',
descriptionEn: '',
descriptionAr: '',
},
});
const onSubmit = async (data: RoleCreateInput) => {
// Data is validated and typed
await createRole(data);
};
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
{/* form fields */}
</form>
);
}
from typing import Generic, TypeVar, List
from api.schemas._base import CamelModel
T = TypeVar('T')
class PaginatedResponse(CamelModel, Generic[T]):
"""Generic paginated response."""
items: List[T]
total: int
page: int
per_page: int
total_pages: int
has_next: bool # → "hasNext"
has_previous: bool # → "hasPrevious"
class UserListResponse(CamelModel):
"""Paginated user list with summary counts."""
items: List[UserResponse]
total: int
page: int
per_page: int
active_count: int # → "activeCount"
inactive_count: int # → "inactiveCount"
// src/my-app/types/common.ts
export interface PaginatedResponse<T> {
items: T[];
total: number;
page: number;
perPage: number;
totalPages: number;
hasNext: boolean;
hasPrevious: boolean;
}
// src/my-app/types/user.ts
export interface UserListResponse {
items: User[];
total: number;
page: number;
perPage: number;
activeCount: number;
inactiveCount: number;
}
from datetime import datetime, timezone
from api.schemas._base import CamelModel
class AuditResponse(CamelModel):
"""Response with datetime fields."""
id: str
action: str
created_at: datetime # Auto-formatted as "2025-01-07T10:30:00Z"
updated_at: Optional[datetime] = None
The CamelModel automatically:
// Dates come as ISO strings with 'Z' suffix
interface AuditLog {
id: string;
action: string;
createdAt: string; // "2025-01-07T10:30:00Z"
updatedAt: string | null;
}
// Parsing helper
function parseDateTime(isoString: string | null): Date | null {
if (!isoString) return null;
return new Date(isoString);
}
// Formatting helper (respects user locale)
function formatDateTime(
isoString: string | null,
locale: string = 'en'
): string {
if (!isoString) return '-';
const date = new Date(isoString);
return date.toLocaleString(locale === 'ar' ? 'ar-SA' : 'en-US', {
dateStyle: 'medium',
timeStyle: 'short',
});
}
DO:
CamelModelOptional[T] = None for nullable fieldsdatetime type for timestamp fieldsDON'T:
BaseModel directlyField(alias="camelCase") manuallydict without by_alias=TrueBefore completing schema work:
CamelModelname_en/name_ar pattern= None default