Payload Validation Middleware
Validates request payloads before they reach your operation handlers. Supports two validation engines: AJV (JSON Schema) and Zod. Pick whichever you prefer -- they're separate imports so you only bundle what you use.
Installation
npm install @strata-js/middleware-payload-validationThen install the validation library you intend to use:
For AJV (JSON Schema):
npm install ajv ajv-formats ajv-errors ajv-keywordsFor Zod (supports v3 and v4):
npm install zodPeer dependency: @strata-js/strata ^2.0.0
Usage
AJV (JSON Schema)
Import from @strata-js/middleware-payload-validation/ajv:
import { StrataContext } from '@strata-js/strata';
import { AjvPayloadValidationMiddleware } from '@strata-js/middleware-payload-validation/ajv';
const users = new StrataContext('users');
const schemas = {
create: {
type: 'object',
required: [ 'email', 'name' ],
properties: {
email: { type: 'string', format: 'email' },
name: { type: 'string', minLength: 1, maxLength: 100 },
age: { type: 'integer', minimum: 0, maximum: 150 },
},
additionalProperties: false,
},
update: {
type: 'object',
required: [ 'userId' ],
properties: {
userId: { type: 'string' },
name: { type: 'string', minLength: 1, maxLength: 100 },
},
additionalProperties: false,
},
};
const validator = new AjvPayloadValidationMiddleware(schemas);
// Register as context-level middleware -- validates every operation in 'users'
users.useMiddleware(validator);
users.registerOperation('create', async (request) =>
{
// Payload is guaranteed to match the 'create' schema here
return createUser(request.payload);
});
users.registerOperation('update', async (request) =>
{
return updateUser(request.payload);
});The schema config is a record where each key matches an operation name and each value is a JSON Schema object. When a request comes in, the middleware looks up the schema by request.operation and validates request.payload against it.
Zod
Import from @strata-js/middleware-payload-validation/zod:
import { StrataContext } from '@strata-js/strata';
import { ZodPayloadValidationMiddleware } from '@strata-js/middleware-payload-validation/zod';
import { z } from 'zod';
const users = new StrataContext('users');
const schemas = {
create: z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
age: z.number().int().min(0).max(150).optional(),
}).strict(),
update: z.object({
userId: z.string(),
name: z.string().min(1).max(100).optional(),
}).strict(),
};
const validator = new ZodPayloadValidationMiddleware(schemas);
users.useMiddleware(validator);
users.registerOperation('create', async (request) =>
{
return createUser(request.payload);
});Both Zod v3 (>= 3.24.2) and Zod v4 are supported, including proper handling of union error formats in both versions.
API
AjvPayloadValidationMiddleware
new AjvPayloadValidationMiddleware(
config : Record<string, Record<string, unknown>>,
ajvOptions ?: AJVOptions,
errorFormatter ?: (errors : DefinedError[]) => unknown
)| Parameter | Type | Description |
|---|---|---|
config | Record<string, Record<string, unknown>> | Map of operation name to JSON Schema. |
ajvOptions | AJVOptions | Custom AJV options. allErrors, $data, and discriminator are always enabled. |
errorFormatter | (errors : DefinedError[]) => unknown | Custom formatter that receives raw AJV DefinedError[] and returns the error details to include in the ValidationError. |
AJV Plugins
The following plugins are automatically installed on every AJV instance:
| Plugin | npm | Description |
|---|---|---|
| ajv-formats | ajv-formats | Format validation (email, uri, date, uuid, etc.) |
| ajv-errors | ajv-errors | Custom error messages via the errorMessage keyword. |
| ajv-keywords | ajv-keywords | Additional validation keywords (transform, uniqueItemProperties, etc.) |
All three are peer dependencies and must be installed alongside ajv.
ZodPayloadValidationMiddleware
new ZodPayloadValidationMiddleware<T>(
schema : ZodPayloadValidationSchema<T>,
errorFormatter ?: (error : ZodError) => unknown
)| Parameter | Type | Description |
|---|---|---|
schema | ZodPayloadValidationSchema<T> | Map of operation name to Zod schema. Each value must be a ZodType. |
errorFormatter | (error : ZodError) => unknown | Custom formatter that receives the raw ZodError and returns the error details to include in the ValidationError. |
Custom Error Formatters
Both middleware classes accept an optional error formatter. Without one, the middleware uses its built-in error parsing. With one, you control exactly what ends up in the ValidationError.details field.
AJV Error Formatter
Receives the raw array of AJV DefinedError objects:
import type { DefinedError } from 'ajv';
const validator = new AjvPayloadValidationMiddleware(
schemas,
{}, // AJV options (defaults)
(errors : DefinedError[]) =>
{
// Return whatever shape you want for error details
return errors.map((e) => ({
field: e.instancePath,
message: e.message,
}));
}
);Without a custom formatter, validation errors are parsed into an array of human-readable strings using the built-in error parser (e.g., "email must match format \"email\"", "must have required property 'name'").
Zod Error Formatter
Receives the full ZodError object:
import type { ZodError } from 'zod';
const validator = new ZodPayloadValidationMiddleware(
schemas,
(error : ZodError) =>
{
return error.issues.map((issue) => ({
path: issue.path.join('.'),
message: issue.message,
}));
}
);Without a custom formatter, Zod errors are parsed into a structured ZodErrorTree:
interface ZodErrorTree
{
errors : string[];
properties ?: Record<string, ZodErrorTree>;
items ?: (ZodErrorTree | null)[];
unionErrors ?: ZodErrorTree[];
}This tree mirrors the structure of the validated data -- nested objects produce nested properties, arrays produce items, and discriminated unions produce unionErrors.
Validation Failure Behavior
When validation fails, the middleware:
Adds a message to
request.messages:typescript{ message: 'Schema validation failed. (See `details` property for more information.)', code: 'payload_validation_error', severity: 'error', type: 'validation_error', details: { validationErrors: /* parsed errors */ } }Calls
request.fail()with aValidationError.Returns the request -- the operation handler is never called.
ValidationError
ValidationError extends ServiceError and includes:
| Property | Value |
|---|---|
code | 'VALIDATION_ERROR' |
message | 'Schema validation failed. (See details property for more information.)' |
details | The parsed validation errors (or the return value of your custom error formatter). |
Operations Without Schemas
If a request arrives for an operation that has no entry in the schema config, the middleware passes the request through without validation. A debug-level log message is emitted:
operation users.delete has no validation schema defined.This lets you use the middleware at the context level even if not every operation needs validation.
Deprecated Export
The default import path re-exports AjvPayloadValidationMiddleware as PayloadValidationMiddleware for backwards compatibility. This will be removed in a future release.
// Deprecated -- will be removed
import { PayloadValidationMiddleware } from '@strata-js/middleware-payload-validation';
// Use these instead
import { AjvPayloadValidationMiddleware } from '@strata-js/middleware-payload-validation/ajv';
import { ZodPayloadValidationMiddleware } from '@strata-js/middleware-payload-validation/zod';Examples
AJV with Custom Options
Enable type coercion so string "42" is accepted for integer fields:
const validator = new AjvPayloadValidationMiddleware(
schemas,
{ coerceTypes: true }
);AJV with Custom Error Messages
Use the errorMessage keyword from ajv-errors to produce friendlier errors:
const schemas = {
create: {
type: 'object',
required: [ 'email', 'name' ],
properties: {
email: { type: 'string', format: 'email' },
name: { type: 'string', minLength: 1 },
},
additionalProperties: false,
errorMessage: {
required: {
email: 'Email address is required',
name: 'Name is required',
},
properties: {
email: 'Must be a valid email address',
},
},
},
};
const validator = new AjvPayloadValidationMiddleware(schemas);Zod with Refinements
import { z } from 'zod';
const schemas = {
transferFunds: z.object({
fromAccount: z.string(),
toAccount: z.string(),
amount: z.number().positive(),
}).refine(
(data) => data.fromAccount !== data.toAccount,
{ message: 'Cannot transfer to the same account' }
),
};
const validator = new ZodPayloadValidationMiddleware(schemas);Operation-Level Registration
Register validation on a single operation instead of the whole context:
const users = new StrataContext('users');
const validator = new AjvPayloadValidationMiddleware({
create: {
type: 'object',
required: [ 'email' ],
properties: {
email: { type: 'string', format: 'email' },
},
},
});
users.registerOperation('create', async (request) =>
{
return createUser(request.payload);
}, [], [ validator ]);