Skip to content

Event Schema Validation

Synode validates generated events against Zod schemas before they reach the output adapter. This catches data quality issues during generation rather than downstream.

Defining Event Schemas

Use defineEventSchema to create a Zod object schema for an event's payload.

ts
import { z } from 'zod';
import { defineEventSchema } from '@synode/core';

const pageViewSchema = defineEventSchema({
  url: z.string().url(),
  title: z.string().min(1),
  referrer: z.string().optional(),
});

const purchaseSchema = defineEventSchema({
  orderId: z.string(),
  total: z.number().positive(),
  currency: z.enum(['USD', 'EUR', 'GBP']),
  items: z.array(
    z.object({
      productId: z.string(),
      quantity: z.number().int().positive(),
    }),
  ),
});

defineEventSchema is a thin wrapper around z.object(). You can also pass any z.ZodType directly.

Configuring Validation

Pass an EventSchemaConfig to generate via the eventSchema option.

Single Schema (All Events)

Apply one schema to every event:

ts
import { generate } from '@synode/core';

await generate(journey, {
  users: 1000,
  eventSchema: {
    schema: pageViewSchema,
    mode: 'strict',
  },
});

Per-Event-Name Schema Map

Map event names to their specific schemas. Events not in the map pass through without validation.

ts
await generate(journeys, {
  users: 1000,
  eventSchema: {
    schema: {
      page_view: pageViewSchema,
      purchase: purchaseSchema,
      add_to_cart: addToCartSchema,
    },
    mode: 'warn',
  },
});

Validation Modes

ModeOn FailureEffect
'strict'Throws SynodeValidationErrorGeneration stops immediately
'warn'Logs summary, keeps eventEvent reaches adapter, failure recorded
'skip'Drops event silentlyEvent never reaches adapter, failure recorded

Default mode is 'strict'.

  • strict: Throws SynodeValidationError on first invalid event. Best for development.
  • warn: Keeps all events, prints summary to stderr. Best for staging/CI.
  • skip: Drops invalid events silently. Best for producing clean output.

SynodeValidationError

Thrown in strict mode. Contains the failed event and structured validation issues.

ts
import { SynodeValidationError } from '@synode/core';

try {
  await generate(journey, {
    users: 100,
    eventSchema: { schema: purchaseSchema, mode: 'strict' },
  });
} catch (err) {
  if (err instanceof SynodeValidationError) {
    console.error('Event:', err.event.name);
    console.error('Issues:', err.issues);
    // issues: [{ path: ['total'], message: 'Expected number, received string', code: 'invalid_type' }]
  }
}

ValidationIssue Shape

ts
interface ValidationIssue {
  path: (string | number)[]; // field path in the payload
  message: string; // human-readable error
  code: string; // Zod error code
}

Telemetry Integration

When both debug: true and eventSchema are configured, the validation summary is included in the telemetry report.

ts
await generate(journey, {
  users: 5000,
  eventSchema: {
    schema: { page_view: pageViewSchema, purchase: purchaseSchema },
    mode: 'warn',
  },
  debug: true,
  telemetryPath: './telemetry.json',
});

The telemetry report includes:

json
{
  "validation": {
    "eventsValidated": 5000,
    "eventsValid": 4850,
    "eventsInvalid": 150,
    "validationErrors": [
      { "eventName": "purchase", "path": "total", "message": "Expected number, received string" }
    ]
  }
}

Validation errors are capped at 50 entries to prevent unbounded memory growth.