Schema Validation with Zod V4¶
Meister Bill uses Zod V4 for runtime type validation and schema definitions across all applications.
Overview¶
All schemas are centralized in the schemas/ package and shared between frontend, backend, and mobile apps.
Zod V4 Migration¶
The codebase has been fully migrated to Zod V4. When working with schemas:
Import from "zod/v4"¶
Always import from "zod/v4" instead of "zod":
// ✅ Correct
import { z } from "zod/v4";
// ❌ Wrong
import { z } from "zod";
Use z.ZodTypeAny for Generic Types¶
// ✅ Correct
interface Config {
schema: z.ZodTypeAny;
}
// ❌ Wrong (Zod V3 syntax)
interface Config {
schema: z.ZodType<any>;
}
Avoid Circular Dependencies¶
Import directly from source files, not from the index file:
// ✅ Good
import { CountryCodeSchema } from "./CountryCodeSchema";
// ❌ Bad - may cause circular dependencies
import { CountryCodeSchema } from ".";
Schema Package Structure¶
schemas/
├── src/
│ ├── index.ts # Main export (respects dependency order)
│ ├── *Schema.ts # Individual schema files
│ ├── *Schema.test.ts # Schema tests
│ ├── AddressFormatters.ts # Country-specific address formatting
│ ├── CountryNames.ts # Country code to name mapping
│ ├── forms/ # Form validation schemas
│ ├── newsletter/ # Newsletter-related schemas
│ └── openapi/ # OpenAPI request/response schemas
└── package.json # Published as @meisterbill/schemas
Usage Examples¶
Defining a Schema¶
// schemas/src/CustomerSchema.ts
import { z } from "zod/v4";
export const customerSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1, "Name is required"),
email: z.string().email("Invalid email"),
type: z.enum(["individual", "business"]),
created_at: z.string().datetime(),
});
export type Customer = z.infer<typeof customerSchema>;
Using Schemas in API¶
// apps/api/src/routes/customers.ts
import { customerSchema } from "@meisterbill/schemas";
app.post("/customers", async (c) => {
const body = await c.req.json();
// Validate request body
const result = customerSchema.safeParse(body);
if (!result.success) {
return c.json({ error: result.error }, 400);
}
const customer = result.data;
// ... save to database
});
Using Schemas in Frontend¶
// apps/web/components/forms/Customer.vue
<script setup lang="ts">
import { customerSchema } from "@meisterbill/schemas";
const form = reactive({
name: "",
email: "",
type: "individual",
});
const validate = () => {
const result = customerSchema.safeParse(form);
if (!result.success) {
errors.value = result.error.flatten().fieldErrors;
return false;
}
return true;
};
</script>
Form Validation Schemas¶
The schemas/src/forms/ directory contains form-specific validation:
// schemas/src/forms/SignUpSchema.ts
import { z } from "zod/v4";
export const signUpSchema = z.object({
email: z.string().email("Invalid email address"),
password: z.string()
.min(8, "Password must be at least 8 characters")
.regex(/[A-Z]/, "Password must contain uppercase letter")
.regex(/[0-9]/, "Password must contain a number"),
terms_accepted: z.literal(true, {
errorMap: () => ({ message: "You must accept the terms" }),
}),
});
export type SignUpForm = z.infer<typeof signUpSchema>;
Testing Schemas¶
All schemas have comprehensive test coverage:
// schemas/src/CustomerSchema.test.ts
import { describe, it, expect } from "vitest";
import { customerSchema } from "./CustomerSchema";
describe("customerSchema", () => {
it("validates valid customer", () => {
const result = customerSchema.safeParse({
id: "123e4567-e89b-12d3-a456-426614174000",
name: "John Doe",
email: "john@example.com",
type: "individual",
created_at: "2025-01-01T00:00:00Z",
});
expect(result.success).toBe(true);
});
it("rejects invalid email", () => {
const result = customerSchema.safeParse({
id: "123e4567-e89b-12d3-a456-426614174000",
name: "John Doe",
email: "invalid-email",
type: "individual",
created_at: "2025-01-01T00:00:00Z",
});
expect(result.success).toBe(false);
expect(result.error?.issues[0].message).toContain("Invalid email");
});
});
Running Schema Tests¶
pnpm --filter @meisterbill/schemas test # Run all schema tests
pnpm --filter @meisterbill/schemas build # Build for distribution
Test Count: 414 passing tests
OpenAPI Integration¶
Schemas are used to generate OpenAPI documentation:
// schemas/src/openapi/CustomerSchemas.ts
import { z } from "zod/v4";
import { extendZodWithOpenApi } from "@asteasolutions/zod-to-openapi";
extendZodWithOpenApi(z);
export const createCustomerRequestSchema = z.object({
name: z.string().openapi({ example: "John Doe" }),
email: z.string().email().openapi({ example: "john@example.com" }),
}).openapi("CreateCustomerRequest");
Best Practices¶
1. Single Source of Truth¶
Define schemas once in the schemas/ package, use everywhere:
// ✅ Good
import { invoiceSchema } from "@meisterbill/schemas";
// ❌ Bad - duplicating schema
const invoiceSchema = z.object({ ... });
2. Use Type Inference¶
Let TypeScript infer types from schemas:
// ✅ Good
export type Invoice = z.infer<typeof invoiceSchema>;
// ❌ Bad - manual type definition
export type Invoice = {
id: string;
// ... duplicate work
};
3. Coerce Types When Needed¶
Use .coerce for automatic type conversion:
const schema = z.object({
age: z.coerce.number(), // Converts "25" → 25
active: z.coerce.boolean(), // Converts "true" → true
});
4. Custom Error Messages¶
Provide clear, user-friendly error messages:
const schema = z.object({
email: z.string().email("Please enter a valid email address"),
password: z.string().min(8, "Password must be at least 8 characters long"),
});
5. Optional vs Nullable¶
Be explicit about optional and nullable fields:
const schema = z.object({
name: z.string(), // Required
nickname: z.string().optional(), // Can be undefined
middle_name: z.string().nullable(), // Can be null
suffix: z.string().nullish(), // Can be null or undefined
});
Common Schemas¶
Key schemas available in the package:
invoiceSchema- Invoice/document validationcustomerSchema- Customer dataaddressSchema- Address with country-specific validationpaymentSchema- Payment recordsprojectSchema- Project configurationworkUnitSchema- Billable work unitsserviceProviderSchema- User/account data
See Also¶
- Project Structure - Schema package location
- Tech Stack - Zod overview
- Testing Guide - Testing schemas