Zodra

Generated Output

What Zodra generates from your Ruby type definitions

Generated Output

Zodra generates TypeScript types and Zod schemas from your Ruby definitions. Run the export task to produce the output files:

rails zodra:export

Type → Zod schema

A Ruby type definition:

app/types/product.rb
Zodra.type :product do
  uuid :id
  string :name, min: 1, max: 255
  decimal :price, min: 0
  string :currency, enum: %w[USD EUR GBP]
  boolean :in_stock, default: true
  array :tags, of: :string
  timestamps
end

Generates:

zodra/types/product.ts
import { z } from "zod";

export const ProductSchema = z.object({
  id: z.uuid(),
  name: z.string().min(1).max(255),
  price: z.number().min(0),
  currency: z.enum(["USD", "EUR", "GBP"]),
  inStock: z.boolean().default(true),
  tags: z.array(z.string()),
  createdAt: z.iso.datetime(),
  updatedAt: z.iso.datetime(),
});

export interface Product {
  id: string;
  name: string;
  price: number;
  currency: "USD" | "EUR" | "GBP";
  inStock: boolean;
  tags: string[];
  createdAt: string;
  updatedAt: string;
}

Key format

Keys are transformed based on the key_format configuration:

# config/initializers/zodra.rb
Zodra.configure do |config|
  config.key_format = :camel  # created_at → createdAt (default)
end

See Configuration for all options.

Enum → Zod enum

Zodra.enum :order_status, values: %i[draft confirmed shipped]
export const OrderStatusSchema = z.enum(["draft", "confirmed", "shipped"]);
export type OrderStatus = z.infer<typeof OrderStatusSchema>;

Union → discriminated union

Zodra.union :payment_method, discriminator: :type do
  variant :card do
    string :last_four
    string :brand
  end
  variant :bank_transfer do
    string :bank_name
  end
end
export const PaymentMethodSchema = z.discriminatedUnion("type", [
  z.object({
    type: z.literal("card"),
    lastFour: z.string(),
    brand: z.string(),
  }),
  z.object({
    type: z.literal("bank_transfer"),
    bankName: z.string(),
  }),
]);

Contract → client definitions

Contracts generate action definitions that the @zodra/client uses for typed API calls:

Each contract gets its own file with params schemas and the contract descriptor:

zodra/contracts/products.ts
import { z } from 'zod';
import { ProductSchema } from '../types/product';

export const IndexProductsParamsSchema = z.object({});
export const ShowProductsParamsSchema = z.object({ id: z.uuid() });
export const CreateProductsParamsSchema = z.object({ ... });

export const ProductsContract = {
  index: { method: 'GET' as const, path: '/products' as const, params: IndexProductsParamsSchema, response: ProductSchema, collection: true as const },
  show: { method: 'GET' as const, path: '/products/:id' as const, params: ShowProductsParamsSchema, response: ProductSchema },
  create: { method: 'POST' as const, path: '/products' as const, params: CreateProductsParamsSchema, response: ProductSchema },
} as const;

The contracts barrel re-exports all contracts:

zodra/contracts/index.ts
import { ProductsContract } from './products';

export const contracts = {
  products: ProductsContract,
} as const;

export const baseUrl = '/api/v1';

Output structure

Generated files are written to the configured output_path (default: app/javascript/types):

app/javascript/types/
├── types/
│   ├── index.ts           # Re-exports all types
│   ├── product.ts         # Zod schema + TypeScript interface
│   ├── order.ts           # Imports from other type files
│   └── ...
├── contracts/
│   ├── index.ts           # Contracts map + baseUrl
│   ├── products.ts        # Params schemas + contract descriptor
│   └── ...
└── index.ts               # Re-exports types/ and contracts/

On this page