Skip to content

Validators

Validators check messages before they reach destinations. They enforce schemas, required fields, data formats, and business rules — catching problems early so invalid data never leaves your pipeline.

Every validator exports a single validate function. Throw an error to reject the message; return normally (or return void) to accept it. Rejected messages are routed to error handling (retry, DLQ, or alert).

export function validate(msg: IntuMessage, ctx?: IntuContext): void;
ParameterTypeDescription
msgIntuMessageThe message to validate (with body, contentType, transport metadata)
ctxIntuContextOptional pipeline context — map variables, logger, channel config
// Minimal passing validator — do nothing or return
export function validate(msg: IntuMessage): void {
// Message is accepted
}
// Reject by throwing; the error message is recorded and the message is routed to error handling
export function validate(msg: IntuMessage): void {
if (msg.body === null || msg.body === undefined) {
throw new Error("Message body is empty");
}
const resource = msg.body as { resourceType?: string };
if (resource.resourceType !== "Patient") {
throw new Error("Expected Patient resource, got: " + resource.resourceType);
}
}

Validators are configured in channel.yaml under the validator key:

validator:
entrypoint: validator.ts

Use a path relative to the channel directory (e.g. validator.ts) or a path from project root.

Verify that an HL7v2 message contains the segments your downstream system expects:

export function validate(msg: IntuMessage): void {
const raw = typeof msg.body === "string" ? msg.body : "";
const errors: string[] = [];
const requiredSegments = ["MSH", "PID", "PV1", "EVN"];
for (const seg of requiredSegments) {
const pattern = new RegExp(`^${seg}\\|`, "m");
if (!pattern.test(raw)) {
errors.push(`Missing required segment: ${seg}`);
}
}
const mshFields = raw.split("\r")[0]?.split("|") ?? [];
if (!mshFields[8]) errors.push("MSH.9 (Message Type) is empty");
if (!mshFields[9]) errors.push("MSH.10 (Message Control ID) is empty");
if (errors.length > 0) {
throw new Error(errors.join("; "));
}
}

Ensure a FHIR Patient resource contains the minimum fields before sending to a FHIR server:

export function validate(msg: IntuMessage): void {
const patient = (typeof msg.body === "object" && msg.body !== null) ? msg.body as Record<string, unknown> : {};
const errors: string[] = [];
if (patient.resourceType !== "Patient") {
errors.push(`Expected resourceType "Patient", got "${patient.resourceType}"`);
}
if (!Array.isArray(patient.identifier) || patient.identifier.length === 0) {
errors.push("At least one identifier is required");
}
if (!Array.isArray(patient.name) || patient.name.length === 0) {
errors.push("At least one name is required");
} else {
const name = patient.name[0] as Record<string, unknown>;
if (!name.family) errors.push("Patient name.family is required");
if (!Array.isArray(name.given) || name.given.length === 0) errors.push("Patient name.given is required");
}
if (!patient.birthDate) {
errors.push("birthDate is required");
} else if (!/^\d{4}-\d{2}-\d{2}$/.test(String(patient.birthDate))) {
errors.push(`birthDate must be YYYY-MM-DD format, got "${patient.birthDate}"`);
}
if (patient.gender && !["male", "female", "other", "unknown"].includes(String(patient.gender))) {
errors.push(`Invalid gender value: "${patient.gender}"`);
}
if (errors.length > 0) {
throw new Error(errors.join("; "));
}
}

Use a JSON schema library for complex structural validation:

import Ajv from "ajv";
const ajv = new Ajv({ allErrors: true });
const schema = {
type: "object",
required: ["patient_id", "mrn", "last_name", "first_name"],
properties: {
patient_id: { type: "string", minLength: 1 },
mrn: { type: "string", pattern: "^MRN-\\d{8}$" },
last_name: { type: "string", minLength: 1 },
first_name: { type: "string", minLength: 1 },
dob: { type: "string", format: "date" },
gender: { type: "string", enum: ["M", "F", "U"] },
},
additionalProperties: false,
};
const validateSchema = ajv.compile(schema);
export function validate(msg: IntuMessage): void {
const data = typeof msg.body === "object" && msg.body !== null ? msg.body : {};
const isValid = validateSchema(data);
if (!isValid && validateSchema.errors?.length) {
const messages = validateSchema.errors.map(
(err) => `${err.instancePath || "/"} ${err.message}`
);
throw new Error(messages.join("; "));
}
}

Enforce organization-specific rules that go beyond structural validation:

export function validate(msg: IntuMessage, ctx: IntuContext): void {
const data = (typeof msg.body === "object" && msg.body !== null) ? msg.body as Record<string, unknown> : {};
const errors: string[] = [];
if (data.admitDate && data.dischargeDate) {
if (new Date(String(data.dischargeDate)) < new Date(String(data.admitDate))) {
errors.push("Discharge date cannot be before admit date");
}
}
const validDepartments = ctx.globalMap.validDepartments as Set<string> | undefined;
if (validDepartments && data.department && !validDepartments.has(String(data.department))) {
errors.push(`Unknown department: "${data.department}"`);
}
if (data.age !== undefined && (Number(data.age) < 0 || Number(data.age) > 150)) {
errors.push(`Age out of plausible range: ${data.age}`);
}
if (errors.length > 0) {
throw new Error(errors.join("; "));
}
}

Validate that observation values fall within clinically plausible ranges:

const labRanges: Record<string, { min: number; max: number; unit: string }> = {
"2339-0": { min: 30, max: 500, unit: "mg/dL" }, // Glucose
"2160-0": { min: 0.1, max: 20, unit: "mg/dL" }, // Creatinine
"6690-2": { min: 1000, max: 50000, unit: "10*3/uL" }, // WBC
"718-7": { min: 3, max: 25, unit: "g/dL" }, // Hemoglobin
};
export function validate(msg: IntuMessage): void {
const obs = (typeof msg.body === "object" && msg.body !== null) ? msg.body as Record<string, unknown> : {};
const errors: string[] = [];
if (obs.resourceType !== "Observation") {
throw new Error("Expected Observation resource");
}
const coding = (obs.code as { coding?: Array<{ system?: string; code?: string }> })?.coding;
const loincCode = coding?.find((c) => c.system === "http://loinc.org")?.code;
if (!loincCode) {
errors.push("Missing LOINC code");
} else {
const range = labRanges[loincCode];
const vq = obs.valueQuantity as { value?: number } | undefined;
if (range && vq?.value !== undefined) {
const val = vq.value;
if (val < range.min || val > range.max) {
errors.push(
`Value ${val} ${range.unit} out of plausible range [${range.min}${range.max}] for LOINC ${loincCode}`
);
}
}
}
if (!(obs.subject as { reference?: string })?.reference) {
errors.push("Observation.subject.reference is required");
}
if (!obs.effectiveDateTime) {
errors.push("Observation.effectiveDateTime is required");
}
if (errors.length > 0) {
throw new Error(errors.join("; "));
}
}

Validators and filters both prevent messages from reaching destinations, but they serve different purposes:

ValidatorFilter (return null from transformer)
IntentReject invalid dataSilently drop unwanted data
FeedbackError record with reasonsNo record — message disappears
MetricsCounted as validation errorsCounted as filtered
RoutingError handling / dead-letter queueNothing

Validator — rejecting bad data (throw):

export function validate(msg: IntuMessage): void {
const patient = (typeof msg.body === "object" && msg.body !== null) ? msg.body as Record<string, unknown> : {};
if (!Array.isArray(patient.identifier) || patient.identifier.length === 0) {
throw new Error("Patient must have at least one identifier");
}
}

Filter — dropping irrelevant messages in a transformer:

export function transform(msg: IntuMessage, ctx: IntuContext): IntuMessage | null {
const raw = typeof msg.body === "string" ? msg.body : "";
const messageType = raw.split("|")[8] ?? "";
if (!messageType.startsWith("ADT^A01")) return null;
return msg;
}