rule-schemas

Rule mapping for schemas

SKILL.md

GitHub

Rule 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 use z.looseObject in layer schemas — each layer should be more specific, not more permissive.
  • z.looseObject for predicate/getter validators only: Narrow structural checks in predicate or getter helpers (e.g. "does this event have a metadata.attributes array?") may use z.looseObject to 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 *RuleSubjectSchema that 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 a z.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.