Savvi Studio

Schema Generation

Purpose: How Zod schemas are generated from PostgreSQL types
Last Updated: 2024-11-28

Overview

The schema generation layer converts PostgreSQL type metadata into Zod validation schemas, providing runtime type safety in addition to compile-time TypeScript types.

For detailed builder implementations: See Schema Builders

Architecture

TypeCatalog (type metadata)
  ↓ processed by
Schema Builders (Enum, Domain, Composite, Array, Range)
  ↓ generate
Zod Schemas (TypeScript code)
  ↓ used for
Runtime Validation

Builder Types

Builder PostgreSQL Type Zod Output Example
EnumBuilder ENUM z.enum([...]) status AS ENUM ('active', 'inactive')
DomainBuilder DOMAIN z.string().refine(...) email AS text CHECK (...)
CompositeBuilder Composite type z.object({...}) user_record AS (id bigint, ...)
ArrayBuilder Array type z.array(...) text[], integer[]
RangeBuilder Range type z.object({...}) daterange, int4range

Detailed implementations: Schema Builders

Generation Process

Steps: Initialize builders → Topological sort → Generate schemas → Emit TypeScript

// 1. Initialize builders for each type category
const builders = [
  new EnumBuilder(typeCatalog),
  new DomainBuilder(typeCatalog),
  new CompositeBuilder(typeCatalog),
  new ArrayBuilder(typeCatalog),
  new RangeBuilder(typeCatalog)
];

// 2. Sort types by dependencies
const sortedTypes = typeCatalog.topologicalSort();

// 3. Generate schemas
for (const type of sortedTypes) {
  const builder = builders.find(b => b.canHandle(type));
  if (builder) emit(builder.build(type));
}

Output Example:

export const statusSchema = z.enum(['active', 'inactive']);
export type Status = z.infer<typeof statusSchema>;

Base Type Mappings

Common PostgreSQL to Zod mappings:

PostgreSQL Zod Schema TypeScript Type
integer z.number().int() number
bigint z.bigint() bigint
text z.string() string
boolean z.boolean() boolean
timestamp z.string().datetime() string
jsonb z.unknown() unknown
uuid z.string().uuid() string
text[] z.array(z.string()) string[]

Complete mapping table: Schema Builders

Nullable and Optional

SQL Pattern Zod Schema TypeScript Type
RETURNS bigint (can be NULL) z.bigint().nullable() bigint | null
p_name text DEFAULT 'x' z.string().optional() string | undefined
p_name text DEFAULT NULL z.string().nullable().optional() string | null | undefined

Nested Types

Composites: Dependencies generated first, then referenced by name

// address first, then user_with_address
export const addressSchema = z.object({ street: z.string(), city: z.string() });
export const userWithAddressSchema = z.object({ id: z.bigint(), address: addressSchema });

Arrays: integer[][]z.array(z.array(z.number().int()))

Runtime Validation

Input: inputSchema.parse(params) - Throws ZodError if invalid
Output: outputSchema.parse(result) - Ensures database returns expected shape

// Input validation
const validated = inputSchema.parse(params);  // ZodError on failure

// Output validation
return userSchema.parse(result.rows[0]);  // ZodError on unexpected data

Error Handling

import { ZodError } from 'zod';

try {
  await createUser(client, { p_email: 'invalid' });
} catch (error) {
  if (error instanceof ZodError) {
    // error.errors: [{ code, message, path, validation }]
    console.error('Validation failed:', error.errors);
  }
}

Code Organization

src/__generated__/graph/
├── types.ts          # Schemas and inferred types
├── functions.ts      # Generated function wrappers
└── index.ts          # Re-exports
// types.ts
export const nodeTypeSchema = z.enum([...]);
export type NodeType = z.infer<typeof nodeTypeSchema>;

Performance

Schema Reuse: Define once, reference everywhere
Lazy Evaluation: z.lazy(() => schema) for circular references

// Reuse
export const emailSchema = z.string().email();
export const userSchema = z.object({ email: emailSchema });

// Circular
export const nodeSchema: z.ZodType = z.lazy(() =>
  z.object({ id: z.bigint(), children: z.array(nodeSchema) })
);

Advanced Patterns

Circular References: Schema Builders - Advanced Patterns

Custom Validators: Schema Builders - Advanced Patterns

Error Handling: Schema Builders - Error Handling

Testing

describe('userSchema', () => {
  it('validates valid user', () => {
    expect(() => userSchema.parse({ id: 1n, email: 'user@example.com' })).not.toThrow();
  });
  
  it('rejects invalid user', () => {
    expect(() => userSchema.parse({ id: 1n, email: 'invalid' })).toThrow(ZodError);
  });
});

More examples: Schema Builders - Testing

Quick Reference

Need See
Builder implementations Schema Builders
Type mappings Type System
Using schemas Function Wrappers
Type discovery Introspection

Core Documentation

Reference


Key Takeaway: Schema builders convert PostgreSQL types to Zod schemas, providing runtime type safety. For detailed builder implementations, see Schema Builders.