Event Connectors

TypeScript schemas

Type-safe Zod schemas for validating Event Connectors API payloads in TypeScript

@eventconnectors/ndtrc_model is the official TypeScript port of the NDTRC domain model, published as Zod schemas. Every entity, enum, and nested type from the canonical Groovy model has a matching Zod schema with field-for-field parity, enforced by automated tests.

Use it to validate API payloads at runtime, infer TypeScript types directly from the schemas (z.infer<typeof TRCItemSchema>), and catch shape drift in CI before it reaches production.

Need to debug one payload quickly? Use the browser-based JSON payload validator to paste a TRCItem draft and get field-level errors without sending the JSON to Event Connectors.

The package is Apache-2.0 licensed. Source lives at github.com/TheFeedFactory/ndtrc_model.

Install

npm install @eventconnectors/ndtrc_model

zod (v3.x) ships as a regular dependency, so it is installed automatically — you don't need to add it separately. Node >=18 is required.

import {
  TRCItemSchema,
  TRCItemResponseSchema,
  CalendarSchema,
  type TRCItem,
  type Event,
} from "@eventconnectors/ndtrc_model";

Every schema and its inferred TypeScript type are exported from the package root.

Validate an API response

Use TRCItemResponseSchema for data coming back from GET /events, GET /locations, GET /venues, or GET /routes. The response variant requires the server-guaranteed fields (trcid, entitytype, creationdate, etc.) that an outbound draft would not yet have.

.safeParse() returns a discriminated { success, data | error } object instead of throwing — which makes the failure path explicit:

import { TRCItemResponseSchema } from "@eventconnectors/ndtrc_model";

const response = await fetch(
  "https://app.eventconnectors.nl/api/events?trcid=e_abc123",
  { headers: { Authorization: `Bearer ${process.env.EVENT_CONNECTORS_TOKEN}` } },
);
const json = await response.json();

const result = TRCItemResponseSchema.safeParse(json[0]);

if (!result.success) {
  console.error("Schema mismatch:", result.error.issues);
  return;
}

const item = result.data;
// item.trcid is `string` (required, never undefined)
// item.entitytype is the `EntityType` literal union
console.log(item.trcid, item.entitytype);

The realistic Koningsdag payload from the TRCItem concept page passes this schema as-is — calendar, location, priceElements and all.

Prefer throwing? Use TRCItemResponseSchema.parse(json[0]) instead — it returns the parsed value on success and throws a ZodError on failure. .safeParse() is recommended for request handlers; .parse() is fine in scripts and tests.

Build a request body

When constructing payloads to POST or PUT, use the primary schema (without the Response suffix). All fields are optional — you only set what you actually want to send:

import { TRCItemSchema } from "@eventconnectors/ndtrc_model";

const draft = TRCItemSchema.safeParse({
  entitytype: "EVENEMENT",
  trcItemDetails: [
    {
      lang: "nl",
      title: "Koningsdag Festival",
      shortdescription: "Jaarlijks festival op Koningsdag",
    },
  ],
  translations: {
    primaryLanguage: "nl",
    availableLanguages: ["nl"],
  },
});

if (!draft.success) {
  throw new Error(`Draft is malformed: ${draft.error.message}`);
}

await fetch("https://app.eventconnectors.nl/api/events", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.EVENT_CONNECTORS_TOKEN}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify(draft.data),
});

Validating before you send catches typos and shape mistakes locally — which is much cheaper to debug than a 400 returned hours later by a feed sync.

Response vs draft schemas

For most entities, one schema is enough — fields are optional and the same shape works for both directions. Two entities have a stricter response variant because the server fills in fields that a client-built draft would not yet have:

EntityDraft schemaResponse schemaWhy a response variant exists
TRCItemTRCItemSchemaTRCItemResponseSchemaServer assigns trcid, entitytype, creationdate, lastupdated, wfstatus
CalendarCalendarSchemaCalendarResponseSchemaServer normalises calendarType and may compute derived singleDates from patterns

Use the draft schema for outbound POST/PUT bodies and the response schema for inbound API data. The response schema is a stricter superset — anything that passes it would also pass the draft schema.

Entity-type aliases

TRCItem is polymorphic — its entitytype field determines whether it represents an Event, Location, Venue, Route, or EventGroup (see EntityType). The package exports five convenience aliases that narrow entitytype to a specific literal:

import type {
  Event,            // TRCItem & { entitytype: "EVENEMENT" }
  LocationItemEntity, // TRCItem & { entitytype: "LOCATIE" }
  Venue,            // TRCItem & { entitytype: "VENUE" }
  Route,            // TRCItem & { entitytype: "ROUTE" }
  EventGroup,       // TRCItem & { entitytype: "EVENTGROUP" }
} from "@eventconnectors/ndtrc_model";

function describePerformers(event: Event): string {
  // event.entitytype is the literal "EVENEMENT" here
  return event.performers?.map((p) => p.label).join(", ") ?? "no performers";
}

These are intersection types, not separate schemas. Validate with TRCItemSchema / TRCItemResponseSchema, then narrow with a check on entitytype:

const result = TRCItemResponseSchema.safeParse(json);
if (result.success && result.data.entitytype === "ROUTE") {
  const route: Route = result.data;
  // route.routeInfo is now reachable without optional chaining
}

Unknown fields: passthrough and strict

Every schema in the package uses Zod's .passthrough() policy by default. Unknown keys in the input are preserved on the parsed output, not stripped. This is deliberate: the Event Connectors API can add new fields without breaking your code, and round-tripping a value through the schema keeps fields you don't yet recognise.

To opt into strict validation — rejecting any unknown key — call .strict() on the schema you care about:

import { GISCoordinateSchema } from "@eventconnectors/ndtrc_model";

const Strict = GISCoordinateSchema.strict();

Strict.parse({ xcoordinate: "5.12", ycoordinate: "52.37" });
// passes

Strict.parse({ xcoordinate: "5.12", ycoordinate: "52.37", typo: true });
// throws ZodError: Unrecognized key(s) in object: 'typo'

Use .strict() for input you control end-to-end (a config file, a test fixture, your own database). Stick with the default for anything that comes from the API or a partner feed — there it's a feature, not a bug, that new fields don't break existing parsers.

Extending schemas with .extend()

If you carry application-specific fields that aren't part of the canonical NDTRC model — a relevance score from your search index, a per-tenant flag, an internal correlation ID — extend the schema rather than duplicating it:

import { TRCItemSchema } from "@eventconnectors/ndtrc_model";
import { z } from "zod";

const InternalTRCItemSchema = TRCItemSchema.extend({
  relevanceScore: z.number().min(0).max(1).optional(),
  tenantId: z.string(),
});

type InternalTRCItem = z.infer<typeof InternalTRCItemSchema>;

.extend() adds fields without losing any of the original schema's validation. The inferred type updates automatically, so downstream code sees relevanceScore and tenantId as first-class properties.

Sub-schemas

The package exports a schema for every entity in the model. The table below lists the top-level schemas — the leaf and helper schemas (PriceDescriptionValueSchema, WhenSchema, PatternDateSchema, etc.) are also exported and discoverable via your editor's autocomplete.

SchemaResponse variant?Concept page
TRCItemSchemaTRCItemResponseSchemaTRCItem
TRCItemGroupSchema
EntityTypeSchemaEntityType
WFStatusSchemaTRCItem § Workflow Status
TranslationsSchemaTranslations
TRCItemDetailSchemaTranslations
TRCItemCategoriesSchemaCategorization
CalendarSchemaCalendarResponseSchemaCalendar
ContactinfoSchemaContactinfo
AddressSchemaAddress
LocationSchemaLocation vs Venue
LocationItemSchema
GISCoordinateSchema
FileSchemaFile
PerformerSchemaPerformer
PriceElementSchemaPriceElement
ExtraPriceInformationSchemaPriceElement
PromotionSchemaPromotion
RouteInfoSchemaRouteInfo
SeoMetadataSchemaSeoMetadata
TrcitemRelationSchema
SubItemGroupSchema
ConvertedEntrySchema
FetchedEntrySchema

For the full export list — including the enum schemas (FileTypeSchema, MediaTypeSchema, URLServiceTypeSchema, RouteTypeSchema, …) and the leaf record schemas — see the package source on GitHub.

On this page