#json-schema #zod #codegen

app domainstack-cli

CLI to generate TypeScript/Zod, JSON Schema, and GraphQL from Rust validation rules. Single source of truth for frontend + backend.

2 stable releases

1.0.1 Jan 7, 2026
1.0.0 Jan 6, 2026

#679 in Procedural macros

MIT/Apache

160KB
3.5K SLoC

domainstack-cli

Blackwell Systems™ Crates.io License: MIT OR Apache-2.0

Code generation CLI for the domainstack full-stack validation ecosystem. Generate TypeScript/Zod schemas, JSON Schema, and OpenAPI specs from your Rust #[validate(...)] attributes.

Overview

domainstack-cli is a command-line tool that transforms Rust types annotated with domainstack validation rules into equivalent schemas for other languages and frameworks. This ensures your validation logic stays synchronized across your entire stack.

# Single source of truth in Rust
#[derive(Validate)]
struct User {
    #[validate(email)]
    #[validate(max_len = 255)]
    email: String,

    #[validate(range(min = 18, max = 120))]
    age: u8,
}

# Generate TypeScript/Zod schemas
domainstack zod --input src --output frontend/schemas.ts

# Generate JSON Schema (Draft 2020-12)
domainstack json-schema --input src --output schemas/types.json

# Generate OpenAPI 3.0/3.1 specification
domainstack openapi --input src --output api/openapi.json

Installation

From crates.io (when published)

cargo install domainstack-cli

From source

# Clone the repository
git clone https://github.com/blackwell-systems/domainstack
cd domainstack/domainstack/domainstack-cli

# Build and install
cargo install --path .

Verify installation

domainstack --version

Quick Start

1. Define your Rust types with validation rules

// src/models.rs
use domainstack::Validate;

#[derive(Validate)]
struct User {
    #[validate(email)]
    #[validate(max_len = 255)]
    email: String,

    #[validate(length(min = 3, max = 50))]
    #[validate(alphanumeric)]
    username: String,

    #[validate(range(min = 18, max = 120))]
    age: u8,

    #[validate(url)]
    profile_url: Option<String>,
}

2. Generate Zod schemas

domainstack zod --input src --output frontend/src/schemas.ts

3. Use the generated schemas in TypeScript

// frontend/src/schemas.ts (auto-generated)
import { z } from "zod";

export const UserSchema = z.object({
  email: z.string().email().max(255),
  username: z.string().min(3).max(50).regex(/^[a-zA-Z0-9]*$/),
  age: z.number().min(18).max(120),
  profile_url: z.string().url().optional(),
});

export type User = z.infer<typeof UserSchema>;

// Use it in your application
const result = UserSchema.safeParse(formData);
if (result.success) {
  // Type-safe validated data
  const user: User = result.data;
}

Commands

domainstack zod

Generate Zod validation schemas from Rust types.

domainstack zod [OPTIONS]

Options:

  • -i, --input <PATH> - Input directory containing Rust source files (default: src)
  • -o, --output <PATH> - Output TypeScript file (required)
  • -w, --watch - Watch for changes and regenerate automatically
  • -v, --verbose - Enable verbose output
  • -h, --help - Print help information

Examples:

# Basic usage
domainstack zod --output schemas.ts

# Specify input directory
domainstack zod --input backend/src --output frontend/schemas.ts

# Verbose output
domainstack zod -i src -o schemas.ts -v

# Watch mode for development
domainstack zod -i src -o schemas.ts --watch

domainstack json-schema

Generate JSON Schema (Draft 2020-12) from Rust types.

domainstack json-schema [OPTIONS]

Options:

  • -i, --input <PATH> - Input directory containing Rust source files (default: src)
  • -o, --output <PATH> - Output JSON file (required)
  • -w, --watch - Watch for changes and regenerate automatically
  • -v, --verbose - Enable verbose output
  • -h, --help - Print help information

Examples:

# Basic usage
domainstack json-schema --output schema.json

# Specify input directory
domainstack json-schema --input backend/src --output api/schemas.json

# Verbose with watch mode
domainstack json-schema -i src -o schema.json -v --watch

Generated output:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$defs": {
    "User": {
      "type": "object",
      "properties": {
        "email": { "type": "string", "format": "email", "maxLength": 255 },
        "age": { "type": "integer", "minimum": 18, "maximum": 120 }
      },
      "required": ["email", "age"],
      "additionalProperties": false
    }
  }
}

domainstack openapi

Generate OpenAPI 3.0/3.1 specification from Rust types.

domainstack openapi [OPTIONS]

Options:

  • -i, --input <PATH> - Input directory containing Rust source files (default: src)
  • -o, --output <PATH> - Output JSON file (required)
  • --openapi-31 - Use OpenAPI 3.1 (default is 3.0)
  • -w, --watch - Watch for changes and regenerate automatically
  • -v, --verbose - Enable verbose output
  • -h, --help - Print help information

Examples:

# Generate OpenAPI 3.0 spec
domainstack openapi --output openapi.json

# Generate OpenAPI 3.1 spec
domainstack openapi --output openapi.json --openapi-31

# Watch mode
domainstack openapi -i src -o openapi.json --watch

Generated output:

{
  "openapi": "3.0.3",
  "info": { "title": "Generated API Schema", "version": "1.0.0" },
  "paths": {},
  "components": {
    "schemas": {
      "User": {
        "type": "object",
        "properties": {
          "email": { "type": "string", "format": "email", "maxLength": 255 },
          "age": { "type": "integer", "minimum": 18, "maximum": 120 }
        },
        "required": ["email", "age"],
        "additionalProperties": false
      }
    }
  }
}

Supported Validation Rules

String Validations

Rust Attribute Zod Output Description
#[validate(email)] .email() Valid email address
#[validate(url)] .url() Valid URL
#[validate(min_len = N)] .min(N) Minimum string length
#[validate(max_len = N)] .max(N) Maximum string length
#[validate(length(min = N, max = M))] .min(N).max(M) String length range
#[validate(non_empty)] .min(1) Non-empty string
#[validate(non_blank)] .trim().min(1) Non-blank string (after trim)
#[validate(alphanumeric)] .regex(/^[a-zA-Z0-9]*$/) Alphanumeric only
#[validate(alpha_only)] .regex(/^[a-zA-Z]*$/) Letters only
#[validate(numeric_string)] .regex(/^[0-9]*$/) Digits only
#[validate(ascii)] .regex(/^[\x00-\x7F]*$/) ASCII characters only
#[validate(starts_with = "prefix")] .startsWith("prefix") Must start with prefix
#[validate(ends_with = "suffix")] .endsWith("suffix") Must end with suffix
#[validate(contains = "substring")] .includes("substring") Must contain substring
#[validate(matches_regex = "pattern")] .regex(/pattern/) Custom regex pattern
#[validate(no_whitespace)] .regex(/^\S*$/) No whitespace allowed

Numeric Validations

Rust Attribute Zod Output Description
#[validate(range(min = N, max = M))] .min(N).max(M) Numeric range
#[validate(min = N)] .min(N) Minimum value
#[validate(max = N)] .max(N) Maximum value
#[validate(positive)] .positive() Must be positive (> 0)
#[validate(negative)] .negative() Must be negative (< 0)
#[validate(non_zero)] .refine(n => n !== 0, ...) Cannot be zero
#[validate(multiple_of = N)] .multipleOf(N) Must be multiple of N
#[validate(finite)] .finite() Must be finite (not NaN/Infinity)

Type Mappings

Rust Type Zod Type Notes
String z.string()
bool z.boolean()
u8, u16, u32, i8, i16, i32, f32, f64 z.number()
u64, u128, i64, i128 z.number() With precision warning comment
Option<T> T.optional() Validations applied to inner type
Vec<T> z.array(T)
Custom types CustomTypeSchema References generated schema

Examples

Multiple Validation Rules

You can apply multiple validation rules to a single field:

#[derive(Validate)]
struct Account {
    #[validate(email)]
    #[validate(max_len = 255)]
    #[validate(non_empty)]
    email: String,

    #[validate(min_len = 8)]
    #[validate(max_len = 128)]
    #[validate(matches_regex = "^(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9])")]
    password: String,
}

Generates:

export const AccountSchema = z.object({
  email: z.string().email().max(255).min(1),
  password: z.string().min(8).max(128).regex(/(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9])/),
});

Optional Fields with Validations

Optional fields have .optional() applied AFTER all validations:

#[derive(Validate)]
struct Profile {
    #[validate(url)]
    website: Option<String>,

    #[validate(length(min = 10, max = 500))]
    bio: Option<String>,
}

Generates:

export const ProfileSchema = z.object({
  website: z.string().url().optional(),  // Correct order
  bio: z.string().min(10).max(500).optional(),
});

Arrays and Collections

#[derive(Validate)]
struct Post {
    tags: Vec<String>,

    #[validate(min = 1)]
    #[validate(max = 100)]
    scores: Vec<u8>,
}

Generates:

export const PostSchema = z.object({
  tags: z.array(z.string()),
  scores: z.array(z.number()).min(1).max(100),
});

Architecture

Unified CLI Design

domainstack-cli is designed as a unified code generation tool with a single binary and multiple subcommands:

domainstack
├── zod          ✅ Generate Zod schemas
├── json-schema  ✅ Generate JSON Schema (Draft 2020-12)
├── openapi      ✅ Generate OpenAPI 3.0/3.1 specification
├── yup          📋 Generate Yup schemas (planned)
├── graphql      📋 Generate GraphQL schemas (planned)
└── prisma       📋 Generate Prisma schemas (planned)

Benefits:

  • Single installation, multiple generators
  • Shared parsing infrastructure (efficient, consistent)
  • Consistent CLI interface across all generators
  • Easy to add new generators

Internal Structure

domainstack-cli/
├── src/
│   ├── main.rs              # CLI entry point with clap
│   ├── commands/            # Subcommand implementations
│   │   ├── zod.rs
│   │   ├── json_schema.rs
│   │   └── openapi.rs
│   ├── parser/              # Shared parsing infrastructure
│   │   ├── mod.rs           # Directory walking
│   │   ├── ast.rs           # Rust AST parsing
│   │   └── validation.rs    # Validation rule extraction
│   └── generators/          # Language-specific generators
│       ├── zod.rs
│       ├── json_schema.rs
│       └── openapi.rs

The parser module (parser/) is shared across all generators, ensuring consistent interpretation of Rust validation rules. Each generator (generators/) contains language-specific transformation logic.

Future Generators

The roadmap includes support for additional generators:

Yup (TypeScript validation)

domainstack yup --input src --output schemas.ts

GraphQL Schema Definition Language

domainstack graphql --input src --output schema.graphql

Prisma Schema

domainstack prisma --input src --output schema.prisma

Documentation

Contributing

Adding a New Generator

To add a new generator (e.g., Yup):

  1. Create generator file: src/generators/yup.rs
  2. Implement generation logic using shared parser types
  3. Create command file: src/commands/yup.rs
  4. Add subcommand to src/main.rs

The shared parser infrastructure (parser::ParsedType, parser::ValidationRule) makes adding new generators straightforward - focus only on the output format transformation.

Development Setup

# Clone repository
git clone https://github.com/blackwell-systems/domainstack
cd domainstack

# Build CLI
cargo build -p domainstack-cli

# Run tests
cargo test -p domainstack-cli

# Install locally
cargo install --path domainstack/domainstack-cli

Troubleshooting

"No types found with validation rules"

Make sure your Rust types:

  1. Have #[derive(Validate)] attribute
  2. Contain at least one #[validate(...)] attribute
  3. Are in .rs files within the input directory

Generated schemas don't match expectations

Run with --verbose flag to see parsing details:

domainstack zod --input src --output schemas.ts --verbose

Large integer precision warnings

JavaScript numbers cannot safely represent integers larger than Number.MAX_SAFE_INTEGER (2^53 - 1). Types like u64, i64, u128, i128 will generate schemas with inline warning comments:

big_number: z.number() /* Warning: Large integers may lose precision in JavaScript */

Consider using strings for large integers in your TypeScript schemas if precision is critical.

License

This project is part of the domainstack workspace. See the root LICENSE file for details.

  • domainstack - Core validation library
  • zod - TypeScript-first schema validation
  • ts-rs - TypeScript type generation (no validation)
  • typeshare - Cross-language type sharing

Dependencies

~3–14MB
~121K SLoC