Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
Data Validation - Input Sanitisation & Schema Patterns
Validation patterns ensuring all data entering the system is validated at boundaries: user input via Zod (frontend), API requests via Pydantic (backend). No unvalidated data crosses a trust boundary.
Description
Defines Zod and Pydantic validation patterns for all data entering the system at trust boundaries. Covers form validation, API request schemas, type-safe contracts, Australian-specific validators (ABN, phone, postcode), and schema composition strategies.
When to Apply
Positive Triggers
Creating or modifying form inputs with user data
Defining API request/response schemas (Pydantic models)
Adding Zod schemas for frontend validation
Reviewing code for missing input validation
Building new API endpoints that accept POST/PUT/PATCH data
User mentions: "validation", "Zod", "Pydantic", "schema", "sanitise", "input"
Working on database model definitions (that is ORM schema, not input validation)
Core Directives
Validate at Boundaries, Trust Internally
[User Input] --Zod--> [Frontend] --fetch--> [API] --Pydantic--> [Service Layer]
^ ^
| |
VALIDATE HERE VALIDATE HERE
Frontend boundary: Every form field validated with Zod before submission
API boundary: Every request body validated with Pydantic before processing
Internal code: Trust validated data — no redundant re-validation inside services
Naming Conventions
Layer
Convention
Example
Frontend Zod schema
camelCase + Schema suffix
loginFormSchema
Frontend inferred type
PascalCase via z.infer
type LoginForm = z.infer<typeof loginFormSchema>
Backend Pydantic model
PascalCase + purpose suffix
DocumentCreateRequest
Shared contract
Same field names both sides
email, password, title
Frontend Patterns (Zod + react-hook-form)
Basic Form Schema
The project already uses this pattern in apps/web/components/auth/login-form.tsx:
import * as z from'zod';
import { zodResolver } from'@hookform/resolvers/zod';
import { useForm } from'react-hook-form';
// 1. Define schemaconst loginFormSchema = z.object({
email: z.string().email('Please enter a valid email address'),
password: z.string().min(6, 'Password must be at least 6 characters'),
});
// 2. Infer type (never define manually)typeLoginForm = z.infer<typeof loginFormSchema>;
// 3. Use with react-hook-formconst form = useForm<LoginForm>({
resolver: zodResolver(loginFormSchema),
defaultValues: { email: '', password: '' },
});
Schema Composition
Build complex schemas from reusable parts:
// Base schemas (reusable)const emailSchema = z.string().email('Please enter a valid email address');
const passwordSchema = z.string().min(6, 'Password must be at least 6 characters');
const abnSchema = z.string().regex(/^\d{11}$/, 'ABN must be 11 digits');
// Composed schemasconst registerFormSchema = z
.object({
email: emailSchema,
password: passwordSchema,
confirmPassword: z.string(),
})
.refine((data) => data.password === data.confirmPassword, {
message: 'Passwords do not match',
path: ['confirmPassword'],
});
API Request Validation
Validate data before sending to the backend:
const documentCreateSchema = z.object({
title: z.string().min(1, 'Title is required').max(255),
content: z.string().min(1, 'Content is required'),
metadata: z.record(z.unknown()).optional(),
});
asyncfunctioncreateDocument(input: unknown): Promise<Document> {
// Validate before sending — fail fast on the clientconst validated = documentCreateSchema.parse(input);
return apiClient.post('/api/documents', validated);
}
Australian-Specific Validators
// Australian Business Number (ABN) with checksumconst abnSchema = z.string().refine(
(val) => {
if (!/^\d{11}$/.test(val)) returnfalse;
const weights = [10, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19];
const digits = val.split('').map(Number);
digits[0] -= 1;
const sum = digits.reduce((acc, d, i) => acc + d * weights[i], 0);
return sum % 89 === 0;
},
{ message: 'Invalid ABN' }
);
// Australian phone numberconst auPhoneSchema = z.string().regex(
/^(\+61|0)[2-478]\d{8}$/,
'Please enter a valid Australian phone number'
);
// Australian postcodeconst postcodeSchema = z.string().regex(/^\d{4}$/, 'Postcode must be 4 digits');
// Date in DD/MM/YYYY formatconst auDateSchema = z.string().regex(
/^\d{2}\/\d{2}\/\d{4}$/,
'Date must be in DD/MM/YYYY format'
);
Backend Patterns (Pydantic)
Request Model Convention
The project defines request models in route files or apps/backend/src/api/schemas/: