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(),
cursor: z.string().optional(), // Cursor pagination
includeArchived: z.coerce.boolean().optional(),
sort: 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()
}).strict();

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

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