| Crates.io | domainstack-cli |
| lib.rs | domainstack-cli |
| version | 1.0.1 |
| created_at | 2026-01-06 14:49:50.938414+00 |
| updated_at | 2026-01-07 23:36:42.808323+00 |
| description | CLI to generate TypeScript/Zod, JSON Schema, and GraphQL from Rust validation rules. Single source of truth for frontend + backend. |
| homepage | |
| repository | https://github.com/blackwell-systems/domainstack |
| max_upload_size | |
| id | 2026018 |
| size | 188,056 |
Code generation CLI for the domainstack full-stack validation ecosystem. Generate TypeScript/Zod schemas, JSON Schema, and OpenAPI specs from your Rust #[validate(...)] attributes.
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
cargo install domainstack-cli
# Clone the repository
git clone https://github.com/blackwell-systems/domainstack
cd domainstack/domainstack/domainstack-cli
# Build and install
cargo install --path .
domainstack --version
// 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>,
}
domainstack zod --input src --output frontend/src/schemas.ts
// 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;
}
domainstack zodGenerate 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 informationExamples:
# 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-schemaGenerate 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 informationExamples:
# 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 openapiGenerate 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 informationExamples:
# 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
}
}
}
}
| 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 |
| 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) |
| 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 |
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 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(),
});
#[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),
});
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:
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.
The roadmap includes support for additional generators:
domainstack yup --input src --output schemas.ts
domainstack graphql --input src --output schema.graphql
domainstack prisma --input src --output schema.prisma
To add a new generator (e.g., Yup):
src/generators/yup.rssrc/commands/yup.rssrc/main.rsThe shared parser infrastructure (parser::ParsedType, parser::ValidationRule) makes adding new generators straightforward - focus only on the output format transformation.
# 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
Make sure your Rust types:
#[derive(Validate)] attribute#[validate(...)] attribute.rs files within the input directoryRun with --verbose flag to see parsing details:
domainstack zod --input src --output schemas.ts --verbose
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.
This project is part of the domainstack workspace. See the root LICENSE file for details.