Skip to main content

Contracts - Schemas & Types

Radish CLI generates Zod schemas and TypeScript types for runtime validation and compile-time type safety.

Generated Files​

For each entity, Radish generates a contract file in .radish/lib/datalayer/contracts/:

contracts/
├── common.ts # Base schemas and types
├── user.ts # User entity contracts
├── task.ts # Task entity contracts
└── index.ts # Barrel export

Schema Naming Convention​

Radish uses a Schema suffix naming pattern to clearly distinguish runtime validators from compile-time types:

  • Zod schemas: camelCase + Schema suffix (e.g., taskSchema, createTaskSchema)
  • TypeScript types: PascalCase without suffix (e.g., Task, CreateTask)

This pattern follows industry standards used by tRPC, Remix, and Prisma.

Generated Schemas Per Entity​

For each entity, Radish generates:

1. Fields Schema​

Business fields only (excludes base fields).

export const taskFieldsSchema = z.object({
title: z.string(),
description: z.string().optional(),
status: z.enum(['todo', 'in_progress', 'done']),
dueDate: z.string().datetime().optional(),
}).strict();

export type TaskFields = z.infer<typeof taskFieldsSchema>;

2. Full Entity Schema​

Complete entity with base fields merged in.

export const taskSchema = taskFieldsSchema.merge(entityBaseSchema).strict();

export type Task = z.infer<typeof taskSchema>;

3. Create Schema​

Fields required for creating a new entity.

export const createTaskSchema = z.object({
title: z.string(),
description: z.string().optional(),
status: z.enum(['todo', 'in_progress', 'done']).optional(),
dueDate: z.string().datetime().optional(),
}).strict();

export type CreateTask = z.infer<typeof createTaskSchema>;

4. Update Schema​

Fields allowed when updating (all optional).

export const updateTaskSchema = z.object({
title: z.string().optional(),
description: z.string().optional(),
status: z.enum(['todo', 'in_progress', 'done']).optional(),
dueDate: z.string().datetime().optional(),
}).strict();

export type UpdateTask = z.infer<typeof updateTaskSchema>;

5. Criteria Schema​

Query parameters for filtering and pagination.

export const taskCriteriaSchema = criteriaBaseSchema.extend({
ownerId: z.string().optional(),
status: z.enum(['todo', 'in_progress', 'done']).optional(),
dueDate: z.string().datetime().optional(),
}).strict();

export type TaskCriteria = z.infer<typeof taskCriteriaSchema>;

Base Schemas​

Common Types​

The common.ts file exports base schemas used across all entities:

// Basic types
export const objectIdStrSchema = z.string().regex(/^[a-f0-9]{24}$/, "ObjectId hex");
export const isoDateSchema = z.string().datetime();

// Base entity schemas
export const entityBaseSchema = z.object({
id: objectIdStrSchema,
ownerId: z.string(),
createdAt: isoDateSchema,
updatedAt: isoDateSchema
});

export const systemEntityBaseSchema = z.object({
id: objectIdStrSchema,
createdAt: isoDateSchema,
updatedAt: isoDateSchema
});

// Content base (extends entityBase)
export const contentBaseSchema = entityBaseSchema.extend({
title: z.string(),
slug: z.string().optional(),
summary: z.string().optional(),
tags: z.array(z.string()).default([]),
status: z.enum(['draft', 'published', 'archived']).default('draft'),
publishedAt: isoDateSchema.optional(),
featuredImage: z.string().optional()
});

// TypeScript types
export type ObjectIdStr = z.infer<typeof objectIdStrSchema>;
export type ISODate = z.infer<typeof isoDateSchema>;
export type EntityBase = z.infer<typeof entityBaseSchema>;
export type SystemEntityBase = z.infer<typeof systemEntityBaseSchema>;
export type ContentBase = z.infer<typeof contentBaseSchema>;

Criteria Base​

Query parameter schema with pagination, sorting, and filtering:

export const criteriaBaseSchema = z.object({
q: z.string().optional(), // Text search
limit: z.coerce.number().int().min(1).max(200).optional(),
page: z.coerce.number().int().min(1).optional(), // Page-based pagination (1-indexed)
cursor: z.string().optional(), // Cursor pagination
skip: z.coerce.number().int().min(0).optional(), // Offset-based pagination
offset: z.coerce.number().int().min(0).optional(), // Alias for skip
sortBy: z.string().optional(), // Flat sort field name
sortDir: z.enum(['asc', 'desc']).optional(), // Flat sort direction
select: z.array(z.string()).optional(), // Field projection array
includeArchived: z.coerce.boolean().optional(),
sort: z.union([
z.string(), // Shorthand: '-createdAt' for descending, 'title' for ascending
z.object({
field: z.string(),
direction: z.enum(['asc', 'desc']),
collation: collationSchema.optional()
})
]).optional(),
populate: z.union([
z.string(), // "authorId,categoryId"
z.array(z.string()), // ["authorId", "categoryId"]
z.object({
paths: z.array(z.string()),
depth: z.coerce.number().int().min(1).max(5).optional()
})
]).optional(),
dateFrom: z.string().optional(), // ISO date string
dateTo: z.string().optional(), // ISO date string
dateMonth: z.string() // Enforced YYYY-MM format
.regex(/^\d{4}-\d{2}$/, 'Format: YYYY-MM')
.optional(),
dateField: z.string().optional(), // Target field (default: 'createdAt')
}).strict();

export type CriteriaBase = z.infer<typeof criteriaBaseSchema>;

Additional criteria fields:

FieldTypeDescription
pagenumberPage-based pagination (1-indexed). Alternative to cursor/skip.
sortBy / sortDirstring / enumFlat sort parameters as an alternative to the structured sort object. Example: sortBy: 'date', sortDir: 'desc'.
selectstring[]Field projection array. Only the specified fields (plus id) are returned.
dateMonthstringMonth filter in strict YYYY-MM format (validated by regex). Expands to a date range internally.

Usage Examples​

Validating User Input​

import { createTaskSchema } from '@generated/datalayer/contracts';

// Validate and parse user input
const input = createTaskSchema.parse(req.body);

// Safe parse (doesn't throw)
const result = createTaskSchema.safeParse(req.body);
if (!result.success) {
console.error(result.error);
}

Type Annotations​

import type { Task, CreateTask, TaskCriteria } from '@generated/datalayer/contracts';

function processTask(task: Task) {
console.log(task.title);
}

async function createTask(input: CreateTask): Promise<Task> {
// ...
}

function findTasks(criteria: TaskCriteria): Promise<Task[]> {
// ...
}

Runtime Validation​

import { taskSchema, updateTaskSchema } from '@generated/datalayer/contracts';

// Validate complete entity
const task = taskSchema.parse(dbRecord);

// Validate partial update
const updates = updateTaskSchema.parse(req.body);

Form Validation​

import { createTaskSchema } from '@generated/datalayer/contracts';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';

const form = useForm({
resolver: zodResolver(createTaskSchema)
});

Enum Generation​

For enum fields with enhanced format (key-label pairs), Radish generates TypeScript enums:

// Blueprint:
// status:
// type: enum
// values:
// - key: todo
// label: To Do
// - key: in_progress
// label: In Progress

// Generated:
export enum TaskStatusEnum {
"todo" = "To Do",
"in_progress" = "In Progress",
"done" = "Done"
}

Versioning Contracts​

If versioning is enabled, additional contracts are generated:

Full Versioning​

export const taskVersionSchema = z.object({
id: z.string(),
entityId: z.string(),
version: z.number().int(),
snapshot: z.any(),
changedBy: z.string(),
changeReason: z.string().optional(),
changeType: z.enum(['create', 'update', 'delete', 'archive', 'restore']),
changedFields: z.array(z.string()).optional(),
createdAt: z.string(),
updatedAt: z.string(),
}).strict();

export type TaskVersion = z.infer<typeof taskVersionSchema>;

Simple Versioning (Audit Log)​

export const taskAuditLogSchema = z.object({
id: z.string(),
entityId: z.string(),
changedBy: z.string(),
changeType: z.enum(['create', 'update', 'delete', 'archive', 'restore']),
changedFields: z.array(z.string()).optional(),
changes: z.record(z.object({
old: z.any(),
new: z.any(),
})).optional(),
changeReason: z.string().optional(),
createdAt: z.string(),
updatedAt: z.string(),
}).strict();

export type TaskAuditLog = z.infer<typeof taskAuditLogSchema>;

Next Steps​