Skip to content

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

bash
npm install @strata-js/middleware-payload-validation

Then install the validation library you intend to use:

For AJV (JSON Schema):

bash
npm install ajv ajv-formats ajv-errors ajv-keywords

For Zod (supports v3 and v4):

bash
npm install zod

Peer dependency: @strata-js/strata ^2.0.0

Usage

AJV (JSON Schema)

Import from @strata-js/middleware-payload-validation/ajv:

typescript
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:

typescript
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

typescript
new AjvPayloadValidationMiddleware(
    config : Record<string, Record<string, unknown>>,
    ajvOptions ?: AJVOptions,
    errorFormatter ?: (errors : DefinedError[]) => unknown
)
ParameterTypeDescription
configRecord<string, Record<string, unknown>>Map of operation name to JSON Schema.
ajvOptionsAJVOptionsCustom AJV options. allErrors, $data, and discriminator are always enabled.
errorFormatter(errors : DefinedError[]) => unknownCustom 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:

PluginnpmDescription
ajv-formatsajv-formatsFormat validation (email, uri, date, uuid, etc.)
ajv-errorsajv-errorsCustom error messages via the errorMessage keyword.
ajv-keywordsajv-keywordsAdditional validation keywords (transform, uniqueItemProperties, etc.)

All three are peer dependencies and must be installed alongside ajv.

ZodPayloadValidationMiddleware

typescript
new ZodPayloadValidationMiddleware<T>(
    schema : ZodPayloadValidationSchema<T>,
    errorFormatter ?: (error : ZodError) => unknown
)
ParameterTypeDescription
schemaZodPayloadValidationSchema<T>Map of operation name to Zod schema. Each value must be a ZodType.
errorFormatter(error : ZodError) => unknownCustom 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:

typescript
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:

typescript
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:

typescript
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:

  1. 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 */ }
    }
  2. Calls request.fail() with a ValidationError.

  3. Returns the request -- the operation handler is never called.

ValidationError

ValidationError extends ServiceError and includes:

PropertyValue
code'VALIDATION_ERROR'
message'Schema validation failed. (See details property for more information.)'
detailsThe 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.

typescript
// 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:

typescript
const validator = new AjvPayloadValidationMiddleware(
    schemas,
    { coerceTypes: true }
);

AJV with Custom Error Messages

Use the errorMessage keyword from ajv-errors to produce friendlier errors:

typescript
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

typescript
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:

typescript
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 ]);