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_modelzod (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:
| Entity | Draft schema | Response schema | Why a response variant exists |
|---|---|---|---|
| TRCItem | TRCItemSchema | TRCItemResponseSchema | Server assigns trcid, entitytype, creationdate, lastupdated, wfstatus |
| Calendar | CalendarSchema | CalendarResponseSchema | Server 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.
| Schema | Response variant? | Concept page |
|---|---|---|
TRCItemSchema | TRCItemResponseSchema | TRCItem |
TRCItemGroupSchema | — | — |
EntityTypeSchema | — | EntityType |
WFStatusSchema | — | TRCItem § Workflow Status |
TranslationsSchema | — | Translations |
TRCItemDetailSchema | — | Translations |
TRCItemCategoriesSchema | — | Categorization |
CalendarSchema | CalendarResponseSchema | Calendar |
ContactinfoSchema | — | Contactinfo |
AddressSchema | — | Address |
LocationSchema | — | Location vs Venue |
LocationItemSchema | — | — |
GISCoordinateSchema | — | — |
FileSchema | — | File |
PerformerSchema | — | Performer |
PriceElementSchema | — | PriceElement |
ExtraPriceInformationSchema | — | PriceElement |
PromotionSchema | — | Promotion |
RouteInfoSchema | — | RouteInfo |
SeoMetadataSchema | — | SeoMetadata |
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.