From 94cd591b398558a380180566cf5ecde742a51b0c Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Wed, 12 Mar 2025 17:15:27 +0100 Subject: [PATCH 1/4] docs(validation): add main docs page --- docs/utilities/validation.md | 562 ++---------------- .../{validation => event-handler}/.gitignore | 0 .../validation/advancedBringAjvInstance.ts | 31 + .../validation/advancedCustomFormats.ts | 44 ++ .../validation/advancedExternalRefs.ts | 25 + .../validation/gettingStartedDecorator.ts | 27 + .../validation/gettingStartedEnvelope.ts | 19 + .../gettingStartedEnvelopeBuiltin.ts | 20 + .../validation/gettingStartedMiddy.ts | 22 + .../validation/gettingStartedStandalone.ts | 27 + .../samples/gettingStartedEnvelopeEvent.json | 13 + .../gettingStartedSQSEnvelopeEvent.json | 36 ++ .../samples/schemaWithCustomFormat.json | 16 + examples/snippets/validation/schemas.ts | 38 ++ .../validation/schemasWithExternalRefs.ts | 43 ++ mkdocs.yml | 1 + 16 files changed, 401 insertions(+), 523 deletions(-) rename examples/snippets/{validation => event-handler}/.gitignore (100%) create mode 100644 examples/snippets/validation/advancedBringAjvInstance.ts create mode 100644 examples/snippets/validation/advancedCustomFormats.ts create mode 100644 examples/snippets/validation/advancedExternalRefs.ts create mode 100644 examples/snippets/validation/gettingStartedDecorator.ts create mode 100644 examples/snippets/validation/gettingStartedEnvelope.ts create mode 100644 examples/snippets/validation/gettingStartedEnvelopeBuiltin.ts create mode 100644 examples/snippets/validation/gettingStartedMiddy.ts create mode 100644 examples/snippets/validation/gettingStartedStandalone.ts create mode 100644 examples/snippets/validation/samples/gettingStartedEnvelopeEvent.json create mode 100644 examples/snippets/validation/samples/gettingStartedSQSEnvelopeEvent.json create mode 100644 examples/snippets/validation/samples/schemaWithCustomFormat.json create mode 100644 examples/snippets/validation/schemas.ts create mode 100644 examples/snippets/validation/schemasWithExternalRefs.ts diff --git a/docs/utilities/validation.md b/docs/utilities/validation.md index bfbaa2aac3..3237cb246c 100644 --- a/docs/utilities/validation.md +++ b/docs/utilities/validation.md @@ -1,15 +1,13 @@ --- -title: Validation (JSON Schema) +title: Validation descrition: Utility +status: new --- This utility provides [JSON Schema](https://json-schema.org) validation for events and responses, including JMESPath support to unwrap events before validation. -!!! warning - This feature is currently under development. As such it's considered not stable and we might make significant breaking changes before going before its release. You are welcome to [provide feedback](https://github.com/aws-powertools/powertools-lambda-typescript/discussions/3519) and [contribute to its implementation](https://github.com/aws-powertools/powertools-lambda-typescript/milestone/18). - ## Key features - Validate incoming event and response payloads @@ -20,7 +18,7 @@ This utility provides [JSON Schema](https://json-schema.org) validation for even ## Getting started ```bash -npm install @aws-lambda-powertools/validation ajv +npm install @aws-lambda-powertools/validation ``` You can validate inbound and outbound payloads using the validator class method decorator or Middy.js middleware. @@ -41,75 +39,16 @@ If the validation fails, we will throw a `SchemaValidationError`. All our decorators assume that the method they are decorating is an async method. This means that even when decorating a synchronous method, it will return a promise. If this is not the desired behavior, you can use one of the other patterns to validate your payloads. -=== "getting_started_decorator.ts" +=== "gettingStartedDecorator.ts" ```typescript - import { validator } from '@aws-lambda-powertools/validation'; - import type { Context } from 'aws-lambda'; - import { - inboundSchema, - outboundSchema, - type InboundSchema, - type OutboundSchema - } from './getting_started_schemas.js'; - - class Lambda { - @validator({ - inboundSchema, - outboundSchema, - }) - async handler(event: InboundSchema, context: Context): Promise { - return { - statusCode: 200, - body: `Hello from ${event.userId}`, - } - } - } - - export const handler = new Lambda().handler + --8<-- "examples/snippets/validation/gettingStartedDecorator.ts" ``` -=== "getting_started_schemas.ts" +=== "schemas.ts" ```typescript - const inboundSchema = { - type: 'object', - properties: { - userId: { - type: 'string' - } - }, - required: ['userId'] - } as const; - - type InboundSchema = { - userId: string; - }; - - const outboundSchema = { - type: 'object', - properties: { - body: { - type: 'string' - }, - statusCode: { - type: 'number' - } - }, - required: ['body', 'statusCode'] - } as const; - - type OutboundSchema = { - body: string; - statusCode: number; - }; - - export { - inboundSchema, - outboundSchema, - type InboundSchema, - type OutboundSchema - }; + --8<-- "examples/snippets/validation/schemas.ts" ``` It's not mandatory to validate both the inbound and outbound payloads. You can either use one, the other, or both. @@ -125,73 +64,16 @@ If you are using Middy.js, you can use the `validator` middleware to validate th Like the class method decorator, if the validation fails, we will throw a `SchemaValidationError`, and you don't need to use both the inbound and outbound schemas if you don't need to. -=== "getting_started_middy.ts" +=== "gettingStartedMiddy.ts" ```typescript - import { validator } from '@aws-lambda-powertools/validation/middleware'; - import middy from '@middy/core'; - import { - inboundSchema, - outboundSchema, - type InboundSchema, - type OutboundSchema - } from './getting_started_schemas.js'; - - export const handler = middy() - .use(validator({ - inboundSchema, - outboundSchema, - })) - .handler( - async (event: InboundSchema, context: Context): Promise => { - return { - statusCode: 200, - body: `Hello from ${event.userId}`, - } - }); + --8<-- "examples/snippets/validation/gettingStartedMiddy.ts" ``` -=== "getting_started_schemas.ts" +=== "schemas.ts" ```typescript - const inboundSchema = { - type: 'object', - properties: { - userId: { - type: 'string' - } - }, - required: ['userId'] - } as const; - - type InboundSchema = { - userId: string; - }; - - const outboundSchema = { - type: 'object', - properties: { - body: { - type: 'string' - }, - statusCode: { - type: 'number' - } - }, - required: ['body', 'statusCode'] - } as const; - - type OutboundSchema = { - body: string; - statusCode: number; - }; - - export { - inboundSchema, - outboundSchema, - type InboundSchema, - type OutboundSchema - }; + --8<-- "examples/snippets/validation/schemas.ts" ``` ### Standalone validation @@ -200,80 +82,16 @@ The `validate` function gives you more control over the validation process, and You can also gracefully handle schema validation errors by catching `SchemaValidationError` errors. -=== "getting_started_standalone.ts" +=== "gettingStartedStandalone.ts" ```typescript - import { validate, SchemaValidationError } from '@aws-lambda-powertools/validation'; - import { Logger } from '@aws-lambda-powertools/logger'; - import { - inboundSchema, - type InboundSchema, - } from './getting_started_schemas.js'; - - const logger = new Logger(); - - export const handler = async (event: InboundSchema, context: Context) => { - try { - await validate({ - payload: event, - schema: inboundSchema, - }) - - return { // since we are not validating the output, we can return anything - message: 'ok' - } - } catch (error) { - if (error instanceof SchemaValidationError) { - logger.error('Schema validation failed', error) - throw new Error('Invalid event payload') - } - - throw error - } - } + --8<-- "examples/snippets/validation/gettingStartedStandalone.ts" ``` -=== "getting_started_schemas.ts" +=== "schemas.ts" ```typescript - const inboundSchema = { - type: 'object', - properties: { - userId: { - type: 'string' - } - }, - required: ['userId'] - } as const; - - type InboundSchema = { - userId: string; - }; - - const outboundSchema = { - type: 'object', - properties: { - body: { - type: 'string' - }, - statusCode: { - type: 'number' - } - }, - required: ['body', 'statusCode'] - } as const; - - type OutboundSchema = { - body: string; - statusCode: number; - }; - - export { - inboundSchema, - outboundSchema, - type InboundSchema, - type OutboundSchema - }; + --8<-- "examples/snippets/validation/schemas.ts" ``` ### Unwrapping events prior to validation @@ -284,92 +102,22 @@ Envelopes are [JMESPath expressions](https://jmespath.org/tutorial.html) to extr Here is a sample custom EventBridge event, where we only want to validate the `detail` part of the event: -=== "getting_started_envelope.ts" +=== "gettingStartedEnvelope.ts" ```typescript - import { validator } from '@aws-lambda-powertools/validation'; - import type { Context } from 'aws-lambda'; - import { - inboundSchema, - type InboundSchema, - type OutboundSchema - } from './getting_started_schemas.js'; - - class Lambda { - @validator({ - inboundSchema, - envelope: 'detail', - }) - async handler(event: InboundSchema, context: Context) { - return { - message: `processed ${event.userId}`, - success: true, - } - } - } - - export const handler = new Lambda().handler + --8<-- "examples/snippets/validation/gettingStartedEnvelope.ts" ``` -=== "getting_started_schemas.ts" +=== "schemas.ts" ```typescript - const inboundSchema = { - type: 'object', - properties: { - userId: { - type: 'string' - } - }, - required: ['userId'] - } as const; - - type InboundSchema = { - userId: string; - }; - - const outboundSchema = { - type: 'object', - properties: { - body: { - type: 'string' - }, - statusCode: { - type: 'number' - } - }, - required: ['body', 'statusCode'] - } as const; - - type OutboundSchema = { - body: string; - statusCode: number; - }; - - export { - inboundSchema, - outboundSchema, - type InboundSchema, - type OutboundSchema - }; + --8<-- "examples/snippets/validation/schemas.ts" ``` -=== "getting_started_envelope_event.json" +=== "gettingStartedEnvelopeEvent.json" ```json - { - "version": "0", - "id": "12345678-1234-1234-1234-123456789012", - "detail-type": "myDetailType", - "source": "myEventSource", - "account": "123456789012", - "time": "2017-12-22T18:43:48Z", - "region": "us-west-2", - "resources": [], - "detail": { - "userId": "123" - } - } + --8<-- "examples/snippets/validation/samples/gettingStartedEnvelopeEvent.json" ``` This is quite powerful as it allows you to validate only the part of the event that you are interested in, and thanks to JMESPath, you can extract records from [arrays](https://jmespath.org/tutorial.html#list-and-slice-projections), combine [pipe](https://jmespath.org/tutorial.html#pipe-expressions) and filter expressions, and more. @@ -382,115 +130,22 @@ We provide built-in envelopes to easily extract payloads from popular AWS event Here is an example of how you can use the built-in envelope for SQS events: -=== "getting_started_envelope_builtin.ts" +=== "gettingStartedEnvelopeBuiltin.ts" ```typescript - import { validator } from '@aws-lambda-powertools/validation'; - import { SQS } from '@aws-lambda-powertools/validation/envelopes/sqs'; - import type { Context } from 'aws-lambda'; - import { - inboundSchema, - type InboundSchema, - } from './getting_started_schemas.js'; - - const logger = new Logger(); - - export const handler = middy() - .use(validator({ - inboundSchema, - envelope: SQS, - })) - .handler( - async (event: Array, context: Context) => { - for (const record of event) { - logger.info(`Processing message ${record.userId}`); - } - } - ) + --8<-- "examples/snippets/validation/gettingStartedEnvelopeBuiltin.ts" ``` -=== "getting_started_schemas.ts" +=== "schemas.ts" ```typescript - const inboundSchema = { - type: 'object', - properties: { - userId: { - type: 'string' - } - }, - required: ['userId'] - } as const; - - type InboundSchema = { - userId: string; - }; - - const outboundSchema = { - type: 'object', - properties: { - body: { - type: 'string' - }, - statusCode: { - type: 'number' - } - }, - required: ['body', 'statusCode'] - } as const; - - type OutboundSchema = { - body: string; - statusCode: number; - }; - - export { - inboundSchema, - outboundSchema, - type InboundSchema, - type OutboundSchema - }; + --8<-- "examples/snippets/validation/schemas.ts" ``` -=== "getting_started_envelope_event.json" +=== "gettingStartedSQSEnvelopeEvent.json" ```json - { - "Records": [ - { - "messageId": "c80e8021-a70a-42c7-a470-796e1186f753", - "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", - "body": "{\"userId\":\"123\"}", - "attributes": { - "ApproximateReceiveCount": "3", - "SentTimestamp": "1529104986221", - "SenderId": "AIDAIC6K7FJUZ7Q", - "ApproximateFirstReceiveTimestamp": "1529104986230" - }, - "messageAttributes": {}, - "md5OfBody": "098f6bcd4621d373cade4e832627b4f6", - "eventSource": "aws:sqs", - "eventSourceARN": "arn:aws:sqs:us-west-2:123456789012:my-queue", - "awsRegion": "us-west-2" - }, - { - "messageId": "c80e8021-a70a-42c7-a470-796e1186f753", - "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", - "body": "{\"userId\":\"456\"}", - "attributes": { - "ApproximateReceiveCount": "3", - "SentTimestamp": "1529104986221", - "SenderId": "AIDAIC6K7FJUZ7Q", - "ApproximateFirstReceiveTimestamp": "1529104986230" - }, - "messageAttributes": {}, - "md5OfBody": "098f6bcd4621d373cade4e832627b4f6", - "eventSource": "aws:sqs", - "eventSourceARN": "arn:aws:sqs:us-west-2:123456789012:my-queue", - "awsRegion": "us-west-2" - } - ] - } + --8<-- "examples/snippets/validation/samples/gettingStartedSQSEnvelopeEvent.json" ``` For a complete list of built-in envelopes, check the built-in envelopes section [here](https://docs.powertools.aws.dev/lambda/typescript/latest/utilities/jmespath/#built-in-envelopes). @@ -505,70 +160,20 @@ This is useful when you have a specific format that is not covered by the built- JSON Schemas with custom formats like `awsaccountid` will fail validation if the format is not defined. You can define custom formats using the `formats` option to any of the validation methods. -=== "schema_with_custom_format.json" +=== "schemaWithCustomFormat.json" ```json - { - "type": "object", - "properties": { - "accountId": { - "type": "string", - "format": "awsaccountid" - }, - "creditCard": { - "type": "string", - "format": "creditcard" - } - }, - "required": ["accountId"] - } + --8<-- "examples/snippets/validation/samples/schemaWithCustomFormat.json" ``` For each one of these custom formats, you need to tell us how to validate them. To do so, you can either pass a `RegExp` object or a function that receives the value and returns a boolean. For example, to validate using the schema above, you can define a custom format for `awsaccountid` like this: -=== "advanced_custom_format.ts" +=== "advancedCustomFormats.ts" ```typescript - import { validate, SchemaValidationError } from '@aws-lambda-powertools/validation'; - import { Logger } from '@aws-lambda-powertools/logger'; - - const logger = new Logger(); - - const customFormats = { - awsaccountid: new RegExp('^[0-9]{12}$'), - creditcard: (value: string) => { - // Luhn algorithm (for demonstration purposes only - do not use in production) - const sum = value.split('').reverse().reduce((acc, digit, index) => { - const num = parseInt(digit, 10); - return acc + (index % 2 === 0 ? num : num < 5 ? num * 2 : num * 2 - 9); - }, 0); - - return sum % 10 === 0; - } - }; - - export const handler = async (event: any, context: Context) => { - try { - await validate({ - payload: event, - schema: schemaWithCustomFormat, - formats: customFormats, - }) - - return { // since we are not validating the output, we can return anything - message: 'ok' - } - } catch (error) { - if (error instanceof SchemaValidationError) { - logger.error('Schema validation failed', error) - throw new Error('Invalid event payload') - } - - throw error - } - } + --8<-- "examples/snippets/validation/advancedCustomFormats.ts" ``` ### Built-in JMESpath functions @@ -589,78 +194,16 @@ JSON Schema allows schemas to reference other schemas using the `$ref` keyword. You can use the `externalRefs` option to pass a list of schemas that you want to reference in your inbound and outbound schemas. -=== "advanced_custom_format.ts" +=== "advancedExternalRefs.ts" ```typescript - import { validate } from '@aws-lambda-powertools/validation'; - import { - inboundSchema, - outboundSchema, - defsSchema, - type InboundSchema, - } from './schemas_with_external_ref.ts'; - - class Lambda { - @validator({ - inboundSchema, - outboundSchema, - externalRefs: [defsSchema], - }) - async handler(event: InboundSchema, context: Context) { - return { - message: `processed ${event.userId}`, - success: true, - } - } - } + --8<-- "examples/snippets/validation/advancedExternalRefs.ts" ``` -=== "schemas_with_external_ref.ts" - - ```ts - const defsSchema = { - $id: 'http://example.com/schemas/defs.json', - definitions: { - int: { type: 'integer' }, - str: { type: 'string' }, - }, - } as const; - - const inboundSchema = { - $id: 'http://example.com/schemas/inbound.json', - type: 'object', - properties: { - userId: { $ref: 'defs.json#/definitions/str' } - }, - required: ['userId'] - } as const; - - type InboundSchema = { - userId: string; - }; - - const outboundSchema = { - $id: 'http://example.com/schemas/outbound.json', - type: 'object', - properties: { - body: { $ref: 'defs.json#/definitions/str' }, - statusCode: { $ref: 'defs.json#/definitions/int' } - }, - required: ['body', 'statusCode'] - } as const; - - type OutboundSchema = { - body: string; - statusCode: number; - }; - - export { - defsSchema, - inboundSchema, - outboundSchema, - type InboundSchema, - type OutboundSchema - }; +=== "schemasWithExternalRefs.ts" + + ```typescript + --8<-- "examples/snippets/validation/schemasWithExternalRefs.ts" ``` ### Bringing your own `ajv` instance @@ -669,37 +212,10 @@ By default, we use JSON Schema draft-07. If you want to use a different draft, y This is also useful if you want to configure `ajv` with custom options like keywords and more. -=== "advanced_custom_format.ts" +=== "advancedBringAjvInstance.ts" ```typescript - import { validate } from '@aws-lambda-powertools/validation'; - import Ajv2019 from "ajv/dist/2019" - import { Logger } from '@aws-lambda-powertools/logger'; - - const logger = new Logger(); - - const ajv = new Ajv2019(); - - export const handler = async (event: any, context: Context) => { - try { - await validate({ - payload: event, - schema: schemaWithCustomFormat, - ajv, - }) - - return { // since we are not validating the output, we can return anything - message: 'ok' - } - } catch (error) { - if (error instanceof SchemaValidationError) { - logger.error('Schema validation failed', error) - throw new Error('Invalid event payload') - } - - throw error - } - } + --8<-- "examples/snippets/validation/advancedBringAjvInstance.ts" ``` ## Should I use this or Parser? diff --git a/examples/snippets/validation/.gitignore b/examples/snippets/event-handler/.gitignore similarity index 100% rename from examples/snippets/validation/.gitignore rename to examples/snippets/event-handler/.gitignore diff --git a/examples/snippets/validation/advancedBringAjvInstance.ts b/examples/snippets/validation/advancedBringAjvInstance.ts new file mode 100644 index 0000000000..1b962c7af2 --- /dev/null +++ b/examples/snippets/validation/advancedBringAjvInstance.ts @@ -0,0 +1,31 @@ +import { Logger } from '@aws-lambda-powertools/logger'; +import { validate } from '@aws-lambda-powertools/validation'; +import { SchemaValidationError } from '@aws-lambda-powertools/validation/errors'; +import Ajv2019 from 'ajv/dist/2019'; +import { inboundSchema } from './schemas.js'; + +const logger = new Logger(); + +const ajv = new Ajv2019(); + +export const handler = async (event: unknown) => { + try { + await validate({ + payload: event, + schema: inboundSchema, + ajv, + }); + + return { + // since we are not validating the output, we can return anything + message: 'ok', + }; + } catch (error) { + if (error instanceof SchemaValidationError) { + logger.error('Schema validation failed', error); + throw new Error('Invalid event payload'); + } + + throw error; + } +}; diff --git a/examples/snippets/validation/advancedCustomFormats.ts b/examples/snippets/validation/advancedCustomFormats.ts new file mode 100644 index 0000000000..f53b0528be --- /dev/null +++ b/examples/snippets/validation/advancedCustomFormats.ts @@ -0,0 +1,44 @@ +import { Logger } from '@aws-lambda-powertools/logger'; +import { validate } from '@aws-lambda-powertools/validation'; +import { SchemaValidationError } from '@aws-lambda-powertools/validation/errors'; +import schemaWithCustomFormat from './samples/schemaWithCustomFormat.json'; + +const logger = new Logger(); + +const customFormats = { + awsaccountid: /^[0-9]{12}$/, + creditcard: (value: string) => { + // Luhn algorithm (for demonstration purposes only - do not use in production) + const sum = value + .split('') + .reverse() + .reduce((acc, digit, index) => { + const num = Number.parseInt(digit, 10); + return acc + (index % 2 === 0 ? num : num < 5 ? num * 2 : num * 2 - 9); + }, 0); + + return sum % 10 === 0; + }, +}; + +export const handler = async (event: unknown) => { + try { + await validate({ + payload: event, + schema: schemaWithCustomFormat, + formats: customFormats, + }); + + return { + // since we are not validating the output, we can return anything + message: 'ok', + }; + } catch (error) { + if (error instanceof SchemaValidationError) { + logger.error('Schema validation failed', error); + throw new Error('Invalid event payload'); + } + + throw error; + } +}; diff --git a/examples/snippets/validation/advancedExternalRefs.ts b/examples/snippets/validation/advancedExternalRefs.ts new file mode 100644 index 0000000000..87ab0824ac --- /dev/null +++ b/examples/snippets/validation/advancedExternalRefs.ts @@ -0,0 +1,25 @@ +import { validator } from '@aws-lambda-powertools/validation/decorator'; +import type { Context } from 'aws-lambda'; +import { + type InboundSchema, + defsSchema, + inboundSchema, + outboundSchema, +} from './schemasWithExternalRefs.js'; + +class Lambda { + @validator({ + inboundSchema, + outboundSchema, + externalRefs: [defsSchema], + }) + async handler(event: InboundSchema, _context: Context) { + return { + message: `processed ${event.userId}`, + success: true, + }; + } +} + +const lambda = new Lambda(); +export const handler = lambda.handler.bind(lambda); diff --git a/examples/snippets/validation/gettingStartedDecorator.ts b/examples/snippets/validation/gettingStartedDecorator.ts new file mode 100644 index 0000000000..50cf98fa14 --- /dev/null +++ b/examples/snippets/validation/gettingStartedDecorator.ts @@ -0,0 +1,27 @@ +import { validator } from '@aws-lambda-powertools/validation/decorator'; +import type { Context } from 'aws-lambda'; +import { + type InboundSchema, + type OutboundSchema, + inboundSchema, + outboundSchema, +} from './schemas.js'; + +class Lambda { + @validator({ + inboundSchema, + outboundSchema, + }) + async handler( + event: InboundSchema, + _context: Context + ): Promise { + return { + statusCode: 200, + body: `Hello from ${event.userId}`, + }; + } +} + +const lambda = new Lambda(); +export const handler = lambda.handler.bind(lambda); diff --git a/examples/snippets/validation/gettingStartedEnvelope.ts b/examples/snippets/validation/gettingStartedEnvelope.ts new file mode 100644 index 0000000000..520e006bed --- /dev/null +++ b/examples/snippets/validation/gettingStartedEnvelope.ts @@ -0,0 +1,19 @@ +import { validator } from '@aws-lambda-powertools/validation/decorator'; +import type { Context } from 'aws-lambda'; +import { type InboundSchema, inboundSchema } from './schemas.js'; + +class Lambda { + @validator({ + inboundSchema, + envelope: 'detail', + }) + async handler(event: InboundSchema, context: Context) { + return { + message: `processed ${event.userId}`, + success: true, + }; + } +} + +const lambda = new Lambda(); +export const handler = lambda.handler.bind(lambda); diff --git a/examples/snippets/validation/gettingStartedEnvelopeBuiltin.ts b/examples/snippets/validation/gettingStartedEnvelopeBuiltin.ts new file mode 100644 index 0000000000..0cd1f788fa --- /dev/null +++ b/examples/snippets/validation/gettingStartedEnvelopeBuiltin.ts @@ -0,0 +1,20 @@ +import { SQS } from '@aws-lambda-powertools/jmespath/envelopes'; +import { Logger } from '@aws-lambda-powertools/logger'; +import { validator } from '@aws-lambda-powertools/validation/middleware'; +import middy from '@middy/core'; +import { type InboundSchema, inboundSchema } from './schemas.js'; + +const logger = new Logger(); + +export const handler = middy() + .use( + validator({ + inboundSchema, + envelope: SQS, + }) + ) + .handler(async (event: Array) => { + for (const record of event) { + logger.info(`Processing message ${record.userId}`); + } + }); diff --git a/examples/snippets/validation/gettingStartedMiddy.ts b/examples/snippets/validation/gettingStartedMiddy.ts new file mode 100644 index 0000000000..9b700ab9cf --- /dev/null +++ b/examples/snippets/validation/gettingStartedMiddy.ts @@ -0,0 +1,22 @@ +import { validator } from '@aws-lambda-powertools/validation/middleware'; +import middy from '@middy/core'; +import { + type InboundSchema, + type OutboundSchema, + inboundSchema, + outboundSchema, +} from './schemas.js'; + +export const handler = middy() + .use( + validator({ + inboundSchema, + outboundSchema, + }) + ) + .handler( + async (event: InboundSchema): Promise => ({ + statusCode: 200, + body: `Hello from ${event.userId}`, + }) + ); diff --git a/examples/snippets/validation/gettingStartedStandalone.ts b/examples/snippets/validation/gettingStartedStandalone.ts new file mode 100644 index 0000000000..9be30051e1 --- /dev/null +++ b/examples/snippets/validation/gettingStartedStandalone.ts @@ -0,0 +1,27 @@ +import { Logger } from '@aws-lambda-powertools/logger'; +import { validate } from '@aws-lambda-powertools/validation'; +import { SchemaValidationError } from '@aws-lambda-powertools/validation/errors'; +import { type InboundSchema, inboundSchema } from './schemas.js'; + +const logger = new Logger(); + +export const handler = async (event: InboundSchema) => { + try { + await validate({ + payload: event, + schema: inboundSchema, + }); + + return { + // since we are not validating the output, we can return anything + message: 'ok', + }; + } catch (error) { + if (error instanceof SchemaValidationError) { + logger.error('Schema validation failed', error); + throw new Error('Invalid event payload'); + } + + throw error; + } +}; diff --git a/examples/snippets/validation/samples/gettingStartedEnvelopeEvent.json b/examples/snippets/validation/samples/gettingStartedEnvelopeEvent.json new file mode 100644 index 0000000000..17c1c5672e --- /dev/null +++ b/examples/snippets/validation/samples/gettingStartedEnvelopeEvent.json @@ -0,0 +1,13 @@ +{ + "version": "0", + "id": "12345678-1234-1234-1234-123456789012", + "detail-type": "myDetailType", + "source": "myEventSource", + "account": "123456789012", + "time": "2017-12-22T18:43:48Z", + "region": "us-west-2", + "resources": [], + "detail": { + "userId": "123" + } +} \ No newline at end of file diff --git a/examples/snippets/validation/samples/gettingStartedSQSEnvelopeEvent.json b/examples/snippets/validation/samples/gettingStartedSQSEnvelopeEvent.json new file mode 100644 index 0000000000..a8df4bf107 --- /dev/null +++ b/examples/snippets/validation/samples/gettingStartedSQSEnvelopeEvent.json @@ -0,0 +1,36 @@ +{ + "Records": [ + { + "messageId": "c80e8021-a70a-42c7-a470-796e1186f753", + "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", + "body": "{\"userId\":\"123\"}", + "attributes": { + "ApproximateReceiveCount": "3", + "SentTimestamp": "1529104986221", + "SenderId": "AIDAIC6K7FJUZ7Q", + "ApproximateFirstReceiveTimestamp": "1529104986230" + }, + "messageAttributes": {}, + "md5OfBody": "098f6bcd4621d373cade4e832627b4f6", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:us-west-2:123456789012:my-queue", + "awsRegion": "us-west-2" + }, + { + "messageId": "c80e8021-a70a-42c7-a470-796e1186f753", + "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", + "body": "{\"userId\":\"456\"}", + "attributes": { + "ApproximateReceiveCount": "3", + "SentTimestamp": "1529104986221", + "SenderId": "AIDAIC6K7FJUZ7Q", + "ApproximateFirstReceiveTimestamp": "1529104986230" + }, + "messageAttributes": {}, + "md5OfBody": "098f6bcd4621d373cade4e832627b4f6", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:us-west-2:123456789012:my-queue", + "awsRegion": "us-west-2" + } + ] +} \ No newline at end of file diff --git a/examples/snippets/validation/samples/schemaWithCustomFormat.json b/examples/snippets/validation/samples/schemaWithCustomFormat.json new file mode 100644 index 0000000000..954fe05ddd --- /dev/null +++ b/examples/snippets/validation/samples/schemaWithCustomFormat.json @@ -0,0 +1,16 @@ +{ + "type": "object", + "properties": { + "accountId": { + "type": "string", + "format": "awsaccountid" + }, + "creditCard": { + "type": "string", + "format": "creditcard" + } + }, + "required": [ + "accountId" + ] +} \ No newline at end of file diff --git a/examples/snippets/validation/schemas.ts b/examples/snippets/validation/schemas.ts new file mode 100644 index 0000000000..60f6a199e5 --- /dev/null +++ b/examples/snippets/validation/schemas.ts @@ -0,0 +1,38 @@ +const inboundSchema = { + type: 'object', + properties: { + userId: { + type: 'string', + }, + }, + required: ['userId'], +} as const; + +type InboundSchema = { + userId: string; +}; + +const outboundSchema = { + type: 'object', + properties: { + body: { + type: 'string', + }, + statusCode: { + type: 'number', + }, + }, + required: ['body', 'statusCode'], +} as const; + +type OutboundSchema = { + body: string; + statusCode: number; +}; + +export { + inboundSchema, + outboundSchema, + type InboundSchema, + type OutboundSchema, +}; diff --git a/examples/snippets/validation/schemasWithExternalRefs.ts b/examples/snippets/validation/schemasWithExternalRefs.ts new file mode 100644 index 0000000000..d8a8a293ce --- /dev/null +++ b/examples/snippets/validation/schemasWithExternalRefs.ts @@ -0,0 +1,43 @@ +const defsSchema = { + $id: 'http://example.com/schemas/defs.json', + definitions: { + int: { type: 'integer' }, + str: { type: 'string' }, + }, +} as const; + +const inboundSchema = { + $id: 'http://example.com/schemas/inbound.json', + type: 'object', + properties: { + userId: { $ref: 'defs.json#/definitions/str' }, + }, + required: ['userId'], +} as const; + +type InboundSchema = { + userId: string; +}; + +const outboundSchema = { + $id: 'http://example.com/schemas/outbound.json', + type: 'object', + properties: { + body: { $ref: 'defs.json#/definitions/str' }, + statusCode: { $ref: 'defs.json#/definitions/int' }, + }, + required: ['body', 'statusCode'], +} as const; + +type OutboundSchema = { + body: string; + statusCode: number; +}; + +export { + defsSchema, + inboundSchema, + outboundSchema, + type InboundSchema, + type OutboundSchema, +}; diff --git a/mkdocs.yml b/mkdocs.yml index 994dc80125..5bfc63b52c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -46,6 +46,7 @@ nav: - utilities/batch.md - utilities/jmespath.md - utilities/parser.md + - utilities/validation.md - API reference: api" - Processes: - Roadmap: roadmap.md From 30bc479634efa3040dcc5b4f9dac1bf3066f1418 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Wed, 12 Mar 2025 17:22:33 +0100 Subject: [PATCH 2/4] docs: add line highlight --- docs/utilities/validation.md | 18 ++++++++++-------- .../validation/advancedCustomFormats.ts | 1 - .../validation/gettingStartedStandalone.ts | 5 ++--- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/utilities/validation.md b/docs/utilities/validation.md index 3237cb246c..486b89fb87 100644 --- a/docs/utilities/validation.md +++ b/docs/utilities/validation.md @@ -41,7 +41,7 @@ If the validation fails, we will throw a `SchemaValidationError`. === "gettingStartedDecorator.ts" - ```typescript + ```typescript hl_lines="1 11-14" --8<-- "examples/snippets/validation/gettingStartedDecorator.ts" ``` @@ -66,7 +66,7 @@ Like the class method decorator, if the validation fails, we will throw a `Schem === "gettingStartedMiddy.ts" - ```typescript + ```typescript hl_lines="1 12-15" --8<-- "examples/snippets/validation/gettingStartedMiddy.ts" ``` @@ -84,10 +84,12 @@ You can also gracefully handle schema validation errors by catching `SchemaValid === "gettingStartedStandalone.ts" - ```typescript + ```typescript hl_lines="2 3 10-13 19" --8<-- "examples/snippets/validation/gettingStartedStandalone.ts" ``` + 1. Since we are not validating the output, we can return anything + === "schemas.ts" ```typescript @@ -104,7 +106,7 @@ Here is a sample custom EventBridge event, where we only want to validate the `d === "gettingStartedEnvelope.ts" - ```typescript + ```typescript hl_lines="8" --8<-- "examples/snippets/validation/gettingStartedEnvelope.ts" ``` @@ -132,7 +134,7 @@ Here is an example of how you can use the built-in envelope for SQS events: === "gettingStartedEnvelopeBuiltin.ts" - ```typescript + ```typescript hl_lines="1 13" --8<-- "examples/snippets/validation/gettingStartedEnvelopeBuiltin.ts" ``` @@ -172,7 +174,7 @@ For example, to validate using the schema above, you can define a custom format === "advancedCustomFormats.ts" - ```typescript + ```typescript hl_lines="29" --8<-- "examples/snippets/validation/advancedCustomFormats.ts" ``` @@ -196,7 +198,7 @@ You can use the `externalRefs` option to pass a list of schemas that you want to === "advancedExternalRefs.ts" - ```typescript + ```typescript hl_lines="14" --8<-- "examples/snippets/validation/advancedExternalRefs.ts" ``` @@ -214,7 +216,7 @@ This is also useful if you want to configure `ajv` with custom options like keyw === "advancedBringAjvInstance.ts" - ```typescript + ```typescript hl_lines="9 16" --8<-- "examples/snippets/validation/advancedBringAjvInstance.ts" ``` diff --git a/examples/snippets/validation/advancedCustomFormats.ts b/examples/snippets/validation/advancedCustomFormats.ts index f53b0528be..e926aff966 100644 --- a/examples/snippets/validation/advancedCustomFormats.ts +++ b/examples/snippets/validation/advancedCustomFormats.ts @@ -30,7 +30,6 @@ export const handler = async (event: unknown) => { }); return { - // since we are not validating the output, we can return anything message: 'ok', }; } catch (error) { diff --git a/examples/snippets/validation/gettingStartedStandalone.ts b/examples/snippets/validation/gettingStartedStandalone.ts index 9be30051e1..3b8ae7aebd 100644 --- a/examples/snippets/validation/gettingStartedStandalone.ts +++ b/examples/snippets/validation/gettingStartedStandalone.ts @@ -7,14 +7,13 @@ const logger = new Logger(); export const handler = async (event: InboundSchema) => { try { - await validate({ + validate({ payload: event, schema: inboundSchema, }); return { - // since we are not validating the output, we can return anything - message: 'ok', + message: 'ok', // (1)! }; } catch (error) { if (error instanceof SchemaValidationError) { From 367fc4415243ebc3ad1387255f5e58be23e26218 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Wed, 12 Mar 2025 17:28:59 +0100 Subject: [PATCH 3/4] chore: added callout --- docs/utilities/validation.md | 2 ++ examples/snippets/validation/advancedBringAjvInstance.ts | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/utilities/validation.md b/docs/utilities/validation.md index 486b89fb87..87038ee15a 100644 --- a/docs/utilities/validation.md +++ b/docs/utilities/validation.md @@ -220,6 +220,8 @@ This is also useful if you want to configure `ajv` with custom options like keyw --8<-- "examples/snippets/validation/advancedBringAjvInstance.ts" ``` + 1. You can pass your own `ajv` instance to any of the validation methods. This is useful if you want to configure `ajv` with custom options like keywords and more. + ## Should I use this or Parser? One of Powertools for AWS Lambda [tenets](../index.md#tenets) is to be progressive. This means that our utilities are designed to be incrementally adopted by customers at any stage of their serverless journey. diff --git a/examples/snippets/validation/advancedBringAjvInstance.ts b/examples/snippets/validation/advancedBringAjvInstance.ts index 1b962c7af2..8d0187e09b 100644 --- a/examples/snippets/validation/advancedBringAjvInstance.ts +++ b/examples/snippets/validation/advancedBringAjvInstance.ts @@ -13,11 +13,10 @@ export const handler = async (event: unknown) => { await validate({ payload: event, schema: inboundSchema, - ajv, + ajv, // (1)! }); return { - // since we are not validating the output, we can return anything message: 'ok', }; } catch (error) { From 3ff820aacd2a6c9185bf38ea8dfdb0567c49eddc Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Wed, 12 Mar 2025 17:30:30 +0100 Subject: [PATCH 4/4] chore: update regex --- examples/snippets/validation/advancedCustomFormats.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/snippets/validation/advancedCustomFormats.ts b/examples/snippets/validation/advancedCustomFormats.ts index e926aff966..ccc3f8610b 100644 --- a/examples/snippets/validation/advancedCustomFormats.ts +++ b/examples/snippets/validation/advancedCustomFormats.ts @@ -6,7 +6,7 @@ import schemaWithCustomFormat from './samples/schemaWithCustomFormat.json'; const logger = new Logger(); const customFormats = { - awsaccountid: /^[0-9]{12}$/, + awsaccountid: /^\d{12}$/, creditcard: (value: string) => { // Luhn algorithm (for demonstration purposes only - do not use in production) const sum = value