rule-schemas
Rule mapping for schemas
SKILL.md.claude/skills/rule-schemas/SKILL.md
GitHubRule schemas
Apply this rule whenever work touches:
*.ts
Zod is the runtime validation library for this project. Schemas serve a dual purpose: they validate data at runtime boundaries and provide the canonical type definition via z.infer.
Schema definition
Define schemas close to the types they describe. A common pattern is to export both from the same file:
import { z } from 'zod';
export const VehicleSchema = z.object({
plate: z.string().min(7),
weightKg: z.number().positive(),
type: z.enum(['truck', 'van', 'car']),
});
export type Vehicle = z.infer<typeof VehicleSchema>;
Never create a separate interface Vehicle that duplicates the schema shape.
Validation strategy
Choose the right parse method based on the trust level of the data:
// External input (API payload, S3 object, SQS message) - handle errors
const result = VehicleSchema.safeParse(rawPayload);
if (!result.success) {
logger.warn('Invalid vehicle payload', result.error.flatten());
return { error: 'INVALID_INPUT' };
}
const vehicle = result.data;
// Internal data (already validated upstream) - let it throw
const vehicle = VehicleSchema.parse(trustedData);
Schema composition
Reuse schemas through composition rather than copy-pasting fields:
const BaseDocumentSchema = z.object({
id: z.string().uuid(),
createdAt: z.string().datetime(),
});
const CertificateSchema = BaseDocumentSchema.extend({
issuer: z.string(),
validUntil: z.string().datetime(),
});
Test data generation
Use zocker and the shared testing utilities to generate test data from schemas:
import { createStubFromSchema } from '@carrot-fndn/shared/testing';
const stubVehicle = createStubFromSchema(VehicleSchema);
This ensures test data always conforms to the current schema shape and evolves automatically when the schema changes.
Schema layering
Document schemas are organized in five layers with progressive strictness:
| Layer | Naming convention | Zod strategy | Purpose |
|---|---|---|---|
| Envelope | LoadedDocumentEnvelopeSchema |
z.object |
Wrapper for loaded documents |
| Inbound | Inbound*Schema |
z.object (strips unknown fields) |
Boundary contract for external data |
| Normalized | *Schema |
z.object (strips unknown fields) |
Strict internal representation |
| Domain | Bold*Schema, MassID*Schema |
z.object with methodology-specific fields |
Methodology-specific extensions |
| Rule | *RuleSubjectSchema |
z.object with exact fields needed |
Exact contract per rule processor |
Key conventions:
- All layers use
z.object: Every layer strips unknown fields on parse. Do not usez.looseObjectin layer schemas — each layer should be more specific, not more permissive. z.looseObjectfor predicate/getter validators only: Narrow structural checks in predicate or getter helpers (e.g. "does this event have ametadata.attributesarray?") may usez.looseObjectto avoid rejecting objects that carry extra fields they don't inspect. These are not layer schemas.- Rule subject schemas are mandatory: Every rule processor must define a
*RuleSubjectSchemathat describes the exact data it needs. validateRuleSubjectOrThrow: Standard entry point for rule subject validation. Use this instead of calling.parse()directly.- Const-object +
z.enum()for value sets: Define allowed values as a const object, then derive az.enum()from its values. Use.extract()to narrow to a subset and.literal()for single-value constraints in deeper layers. Domain-scoped enums use declaration merging (const + type + schema) to provide both dot-notation access and a Zod schema:
// Domain enum with declaration merging (schema-first)
export const BoldVehicleType = {
BICYCLE: 'Bicycle',
TRUCK: 'Truck',
} as const;
export const BoldVehicleTypeSchema = z.enum(Object.values(BoldVehicleType) as [string, ...string[]]);
// eslint-disable-next-line no-redeclare
export type BoldVehicleType = z.infer<typeof BoldVehicleTypeSchema>;
// Usage: dot-notation in value position, type in type position
const vehicle = BoldVehicleType.TRUCK;
const isValid: BoldVehicleType = 'Bicycle';
// Narrower layer
const TruckOnlySchema = BoldVehicleTypeSchema.extract(['Truck']);
Do not use TypeScript enum — always use the const-object + z.enum() pattern above.