Summary
Implement Zod schema generation from Forge invariants using the structured expression parser. Extends Zod generator to produce runtime validation code that enforces field constraints and invariant rules.
Motivation
The DB Schema IR foundation and invariant parser create an opportunity for robust runtime validation. Currently, generated Zod schemas validate types but not business constraints. Example:
contract Produto {
valor: decimal
invariant valor > 0
invariant valor < 1000000
}
Should generate:
export const ProdutoSchema = z.object({
valor: z.number()
.refine(v => v > 0, "valor must be > 0")
.refine(v => v < 1000000, "valor must be < 1000000")
});
This enables:
- Compile-time type safety
- Runtime constraint validation
- Consistent validation across all backends (CLI, API, services)
- Clear error messages for consumers
Scope
In Scope
Out of Scope
- Custom validation functions
- Async validators (
.refine(async ...))
- Complex invariant expressions (AND/OR)
- Custom error message i18n
Technical Design
Type Mapping
Operator → Zod Validation
== → .refine(v => v === value)
!= → .refine(v => v !== value)
< → .refine(v => v < value)
> → .refine(v => v > value)
<= → .refine(v => v <= value)
>= → .refine(v => v >= value)
Implementation
-
New Module: packages/generators/src/invariants.ts
function generateZodConstraint(invariant: InvariantExpression): string
function applyInvariants(field: FieldModel, invariants: InvariantModel[]): string
-
Integration Point: Modify generateZodContract() in index.ts
// For each field, find matching invariants
const fieldInvariants = contract.invariants.filter(inv =>
inv.parsed?.left === field.name
);
// Apply constraints to field schema
-
Error Messages:
`${field.name} must satisfy: ${invariant.expression}`
Edge Cases Handled
- Optional fields: Constraint only applied if value provided
- Multiple invariants on same field: Chained with
.refine().refine()
- String values: Quote in error message
- Numeric comparisons: Handle int/float/decimal
- Field references: Resolve field name to value path
Acceptance Criteria
Testing Strategy
Unit Tests
// tests/zod-constraints.test.mjs
test('generate refine for > operator', () => {
const invariant = parseInvariant('valor > 0');
const zod = generateZodConstraint(invariant);
expect(zod).toContain('refine(v => v > 0)');
})
test('validation passes for valid value', () => {
const schema = generateSchema(productContract);
const valid = { valor: 100 };
expect(schema.parse(valid)).toEqual(valid);
})
test('validation fails for invalid value', () => {
const schema = generateSchema(productContract);
const invalid = { valor: -10 };
expect(() => schema.parse(invalid)).toThrow();
})
test('optional field: constraint skipped for undefined', () => {
const schema = generateSchema(userWithOptionalAge);
const noAge = { nome: 'John' };
expect(schema.parse(noAge)).toEqual(noAge);
})
test('multiple invariants chained', () => {
const contract = {
fields: [{ name: 'valor', type: 'decimal', optional: false }],
invariants: [
{ left: 'valor', operator: '>', right: 0 },
{ left: 'valor', operator: '<', right: 1000000 }
]
};
const schema = generateSchema(contract);
expect(schema.parse({ valor: 500 })).toBeDefined();
expect(() => schema.parse({ valor: -10 })).toThrow();
expect(() => schema.parse({ valor: 2000000 })).toThrow();
})
Integration Tests
- Generate full Zod schema with invariants
- Import generated schema into test file
- Execute validation test suite
- Verify TypeScript compilation
Performance Tests
- <10ms for typical contract (20 fields, 30 invariants)
- <100ms for large contract (100 fields, 200 invariants)
Files to Create/Modify
New Files:
packages/generators/src/invariants.ts (120 lines)
tests/zod-constraints.test.mjs (200 lines)
Modified Files:
packages/generators/src/index.ts (lines ~60, add invariant handling)
Non-Goals
- SQL constraint generation (Feature 1.1.3 - database layer)
- OpenAPI documentation with constraints (Feature 1.1.4)
- NestJS validation pipes (Feature 1.1.3)
- UI/form validation
Future Extensions
- Async validation with external services
- Cross-field validation:
validate(obj) => validateCrossFields(obj)
- Custom error formatters
- i18n error messages
- Localization support
Dependencies
Blocks:
- Feature 1.1.3: NestJS Validation Pipes (depends on this)
- Feature 1.1.4: OpenAPI Documentation (depends on this)
Depends On:
Implementation Notes
Type System Consideration
When reading invariants, be defensive:
const parsed = invariant.parsed || parseInvariant(invariant.expression);
This handles the transition period where some invariants may not be parsed yet.
Zod Validation Pattern
// Pattern for numeric comparison
z.number().refine(
v => v > 0,
{ message: "valor must be > 0" }
)
// Pattern for string comparison
z.string().refine(
v => v.length > 0,
{ message: "field must be non-empty" }
)
Testing with Zod InferType
// Verify generated type matches expectation
type GeneratedType = z.infer<typeof GeneratedSchema>;
type Expected = { valor: number };
// TypeScript should not error if they match
Summary
Implement Zod schema generation from Forge invariants using the structured expression parser. Extends Zod generator to produce runtime validation code that enforces field constraints and invariant rules.
Motivation
The DB Schema IR foundation and invariant parser create an opportunity for robust runtime validation. Currently, generated Zod schemas validate types but not business constraints. Example:
Should generate:
This enables:
Scope
In Scope
.refine()for each invariantOut of Scope
.refine(async ...))Technical Design
Type Mapping
Implementation
New Module:
packages/generators/src/invariants.tsIntegration Point: Modify
generateZodContract()inindex.tsError Messages:
`${field.name} must satisfy: ${invariant.expression}`Edge Cases Handled
.refine().refine()Acceptance Criteria
.refine()for each invariantTesting Strategy
Unit Tests
Integration Tests
Performance Tests
Files to Create/Modify
New Files:
packages/generators/src/invariants.ts(120 lines)tests/zod-constraints.test.mjs(200 lines)Modified Files:
packages/generators/src/index.ts(lines ~60, add invariant handling)Non-Goals
Future Extensions
validate(obj) => validateCrossFields(obj)Dependencies
Blocks:
Depends On:
Implementation Notes
Type System Consideration
When reading invariants, be defensive:
This handles the transition period where some invariants may not be parsed yet.
Zod Validation Pattern
Testing with Zod InferType