CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-aws-lambda-powertools--validation

An utility to validate events and responses using JSON Schemas

Overview
Eval results
Files

AWS Lambda Powertools Validation

JSON Schema validation for AWS Lambda events and responses using Ajv. Supports JMESPath data extraction, custom formats, external schema references, and three integration patterns: standalone function, Middy.js middleware, and TypeScript decorator.

Package: @aws-lambda-powertools/validation (npm) Installation: npm install @aws-lambda-powertools/validation

Imports

// Standalone validation
import { validate } from '@aws-lambda-powertools/validation';

// Middy.js middleware
import { validator } from '@aws-lambda-powertools/validation/middleware';

// TypeScript decorator
import { validator } from '@aws-lambda-powertools/validation/decorator';

// Error classes
import {
  ValidationError,
  SchemaValidationError,
  SchemaCompilationError
} from '@aws-lambda-powertools/validation/errors';

// Types (from Ajv)
import type { Ajv, AnySchema, Format } from 'ajv';

Core API

validate() - Standalone Function

Validates payload against JSON schema, returns typed data.

function validate<T = unknown>(params: ValidateParams): T;

type ValidateParams = {
  payload: unknown;              // Data to validate
  schema: AnySchema;             // JSON schema (Draft-07)
  envelope?: string;             // JMESPath to extract nested data
  formats?: Record<string, Format>; // Custom format validators
  externalRefs?: AnySchema | AnySchema[]; // External schemas for $ref
  ajv?: Ajv;                     // Pre-configured Ajv instance
};

Throws: SchemaValidationError (validation fails), SchemaCompilationError (invalid schema)

Example:

import { validate } from '@aws-lambda-powertools/validation';
import { SchemaValidationError } from '@aws-lambda-powertools/validation/errors';

const schema = {
  type: 'object',
  properties: {
    name: { type: 'string' },
    age: { type: 'number', minimum: 0 },
  },
  required: ['name', 'age'],
} as const;

export const handler = async (event: unknown) => {
  try {
    const data = validate({ payload: event, schema });
    return { statusCode: 200, body: JSON.stringify(data) };
  } catch (error) {
    if (error instanceof SchemaValidationError) {
      return {
        statusCode: 400,
        body: JSON.stringify({ error: 'Invalid input', details: error.cause }),
      };
    }
    throw error;
  }
};

validator() - Middy.js Middleware

Validates inbound events (before handler) and/or outbound responses (after handler).

function validator(options: ValidatorOptions): {
  before: MiddlewareFn;
  after: (handler: MiddyLikeRequest) => void;
};

interface ValidatorOptions {
  inboundSchema?: AnySchema;     // Validate incoming event
  outboundSchema?: AnySchema;    // Validate response
  envelope?: string;             // JMESPath for data extraction
  formats?: Record<string, Format>;
  externalRefs?: AnySchema | AnySchema[];
  ajv?: Ajv;
}

Throws: SchemaValidationError with message "Inbound schema validation failed" or "Outbound schema validation failed"

Example:

import { validator } from '@aws-lambda-powertools/validation/middleware';
import middy from '@middy/core';

const inboundSchema = {
  type: 'object',
  properties: {
    userId: { type: 'string' },
    action: { type: 'string' },
  },
  required: ['userId', 'action'],
};

const outboundSchema = {
  type: 'object',
  properties: {
    statusCode: { type: 'number' },
    body: { type: 'string' },
  },
  required: ['statusCode', 'body'],
};

export const handler = middy()
  .use(validator({ inboundSchema, outboundSchema }))
  .handler(async (event) => {
    // event validated, response will be validated
    return {
      statusCode: 200,
      body: JSON.stringify({ success: true }),
    };
  });

@validator() - TypeScript Decorator

Validates class method input (first argument) and/or output (return value).

function validator(options: ValidatorOptions): PropertyDescriptor;

Throws: SchemaValidationError with message "Inbound schema validation failed" or "Outbound schema validation failed"

Example:

import { validator } from '@aws-lambda-powertools/validation/decorator';
import type { Context } from 'aws-lambda';

const inboundSchema = {
  type: 'object',
  properties: {
    value: { type: 'number' },
  },
  required: ['value'],
};

const outboundSchema = {
  type: 'object',
  properties: {
    result: { type: 'number' },
  },
  required: ['result'],
};

class Lambda {
  @validator({ inboundSchema, outboundSchema })
  async handler(event: { value: number }, _context: Context) {
    return { result: event.value * 2 };
  }
}

const lambda = new Lambda();
export const handler = lambda.handler.bind(lambda);

Common Patterns

JMESPath Envelope Extraction

Extract nested data before validation:

// EventBridge detail
const detailSchema = { /* schema for detail object */ };

export const handler = middy()
  .use(validator({
    inboundSchema: detailSchema,
    envelope: 'detail', // Extracts event.detail
  }))
  .handler(async (event) => { /* ... */ });

// API Gateway body
envelope: 'body'

// SQS/SNS message
envelope: 'Records[0].body'
envelope: 'Records[0].Sns.Message'

Custom Format Validators

const customFormats = {
  uuid: (value: string) =>
    /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value),
  positiveInt: (value: number) => Number.isInteger(value) && value > 0,
};

const schema = {
  type: 'object',
  properties: {
    id: { type: 'string', format: 'uuid' },
    quantity: { type: 'number', format: 'positiveInt' },
  },
};

validate({ payload, schema, formats: customFormats });

External Schema References

const addressSchema = {
  $id: 'https://example.com/address.json',
  type: 'object',
  properties: {
    street: { type: 'string' },
    city: { type: 'string' },
  },
  required: ['street', 'city'],
};

const userSchema = {
  type: 'object',
  properties: {
    name: { type: 'string' },
    address: { $ref: 'address.json' },
  },
  required: ['name', 'address'],
};

validate({
  payload,
  schema: userSchema,
  externalRefs: [addressSchema],
});

Custom Ajv Instance (Performance)

Reuse Ajv instance across invocations for better performance:

import Ajv from 'ajv';
import addFormats from 'ajv-formats';

// Outside handler (Lambda warm starts)
const ajvInstance = new Ajv({
  allErrors: true,
  coerceTypes: true,
  useDefaults: true,
});
addFormats(ajvInstance);

export const handler = async (event: unknown) => {
  const data = validate({
    payload: event,
    schema: mySchema,
    ajv: ajvInstance, // Reuse compiled schemas
  });
  return { statusCode: 200, body: JSON.stringify(data) };
};

API Gateway REST API

const requestSchema = {
  type: 'object',
  properties: {
    body: { type: 'string' },
    pathParameters: {
      type: 'object',
      properties: { id: { type: 'string' } },
      required: ['id'],
    },
  },
  required: ['body', 'pathParameters'],
};

const responseSchema = {
  type: 'object',
  properties: {
    statusCode: { type: 'number' },
    body: { type: 'string' },
  },
  required: ['statusCode', 'body'],
};

export const handler = middy()
  .use(validator({ inboundSchema: requestSchema, outboundSchema: responseSchema }))
  .handler(async (event) => {
    const body = JSON.parse(event.body);
    const { id } = event.pathParameters;
    return {
      statusCode: 200,
      body: JSON.stringify({ id, data: body }),
    };
  });

SQS Event Processing

const recordSchema = {
  type: 'object',
  properties: {
    orderId: { type: 'string' },
    items: {
      type: 'array',
      items: {
        type: 'object',
        properties: {
          productId: { type: 'string' },
          quantity: { type: 'number' },
        },
        required: ['productId', 'quantity'],
      },
    },
  },
  required: ['orderId', 'items'],
};

export const handler = async (event: SQSEvent) => {
  for (const record of event.Records) {
    const body = JSON.parse(record.body);
    validate({ payload: body, schema: recordSchema });
    // Process validated record
  }
  return { statusCode: 200 };
};

Combining with Other Middy Middleware

import httpJsonBodyParser from '@middy/http-json-body-parser';
import httpErrorHandler from '@middy/http-error-handler';

export const handler = middy()
  .use(httpJsonBodyParser())      // Parse JSON first
  .use(validator({ inboundSchema })) // Then validate
  .use(httpErrorHandler())         // Handle errors
  .handler(async (event) => { /* ... */ });

Error Handling

Error Class Hierarchy

class ValidationError extends Error {
  name: string;
  message: string;
  cause?: unknown;
}

class SchemaValidationError extends ValidationError {
  name: 'SchemaValidationError';
  cause?: unknown; // Contains Ajv ErrorObject[]
}

class SchemaCompilationError extends ValidationError {
  name: 'SchemaCompilationError';
  message: 'Failed to compile schema';
}

Error Messages

  • SchemaValidationError:

    • "Schema validation failed" - standalone validate()
    • "Inbound schema validation failed" - middleware/decorator input
    • "Outbound schema validation failed" - middleware/decorator output
  • SchemaCompilationError:

    • "Failed to compile schema" - invalid schema

Handling Specific Errors

import { SchemaValidationError, SchemaCompilationError } from '@aws-lambda-powertools/validation/errors';

try {
  const data = validate({ payload: event, schema: mySchema });
  return { statusCode: 200, body: JSON.stringify(data) };
} catch (error) {
  if (error instanceof SchemaCompilationError) {
    // Developer error - invalid schema
    console.error('Schema compilation failed:', error.cause);
    return { statusCode: 500, body: 'Internal error' };
  }

  if (error instanceof SchemaValidationError) {
    // Client error - invalid input
    const ajvErrors = error.cause; // Array of Ajv ErrorObject
    return {
      statusCode: 400,
      body: JSON.stringify({ error: 'Validation failed', details: ajvErrors }),
    };
  }

  throw error;
}

Ajv ErrorObject Structure

interface ErrorObject {
  keyword: string;        // e.g., "required", "type", "minimum"
  instancePath: string;   // JSON Pointer to failing property
  schemaPath: string;     // JSON Pointer to schema rule
  params: Record<string, any>; // Keyword-specific params
  message?: string;       // Human-readable message
  data?: unknown;         // The invalid data
}

Example Errors:

// Missing required property
{ keyword: 'required', params: { missingProperty: 'name' }, message: "must have required property 'name'" }

// Type mismatch
{ keyword: 'type', instancePath: '/age', params: { type: 'number' }, message: 'must be number', data: 'invalid' }

// Range violation
{ keyword: 'minimum', instancePath: '/age', params: { comparison: '>=', limit: 0 }, message: 'must be >= 0', data: -5 }

Middleware Error Handler

import middy from '@middy/core';
import type { MiddyLikeRequest } from '@middy/core';

const errorHandler = (): middy.MiddlewareObj => ({
  onError: async (request: MiddyLikeRequest) => {
    const { error } = request;

    if (error instanceof SchemaValidationError) {
      return {
        statusCode: 400,
        body: JSON.stringify({
          message: error.message,
          errors: error.cause,
        }),
      };
    }

    throw error; // Propagate other errors
  },
});

export const handler = middy()
  .use(validator({ inboundSchema }))
  .use(errorHandler())
  .handler(async (event) => { /* ... */ });

Types Reference

// Re-exported from Ajv (import from 'ajv' package)
import type { Ajv, AnySchema, Format } from 'ajv';

type ValidateParams = {
  payload: unknown;
  schema: AnySchema;
  envelope?: string;
  formats?: Record<string, Format>;
  externalRefs?: AnySchema | AnySchema[];
  ajv?: Ajv;
};

interface ValidatorOptions extends Omit<ValidateParams, 'payload' | 'schema'> {
  inboundSchema?: AnySchema;
  outboundSchema?: AnySchema;
}

// Ajv Format type
type Format =
  | ((data: unknown) => boolean)
  | {
      type?: 'string' | 'number';
      validate: (data: unknown) => boolean;
      async?: boolean;
      compare?: (data1: unknown, data2: unknown) => number;
    };

Key Behaviors

  • Built on Ajv v8: JSON Schema Draft-07 validation
  • JMESPath Integration: Uses @aws-lambda-powertools/jmespath for data extraction
  • Type Safety: Generic return types for TypeScript inference
  • Event Cloning: Middleware clones events before validation to prevent mutation
  • Schema Caching: Ajv compiles and caches schemas; reuse Ajv instances for best performance
  • Decorator Requirements: Must be async methods, always bind to class instance when exporting
  • Multiple Validators: Both inbound/outbound schemas are optional; provide at least one

Install with Tessl CLI

npx tessl i tessl/npm-aws-lambda-powertools--validation
Workspace
tessl
Visibility
Public
Created
Last updated
Describes
npmpkg:npm/@aws-lambda-powertools/validation@2.29.x