From 9af077a058c89a5fcf9454893b08071bba89c948 Mon Sep 17 00:00:00 2001
From: Vatsal Goel <144617902+VatsalGoel3@users.noreply.github.com>
Date: Sat, 1 Mar 2025 00:25:09 -0700
Subject: [PATCH 1/6] feat(validation): add @validator decorator for JSON
 Schema validation

---
 packages/validation/src/decorator.ts          | 79 ++++++++++++++++
 .../validation/tests/unit/decorator.test.ts   | 90 +++++++++++++++++++
 2 files changed, 169 insertions(+)
 create mode 100644 packages/validation/src/decorator.ts
 create mode 100644 packages/validation/tests/unit/decorator.test.ts

diff --git a/packages/validation/src/decorator.ts b/packages/validation/src/decorator.ts
new file mode 100644
index 0000000000..d92c8bf407
--- /dev/null
+++ b/packages/validation/src/decorator.ts
@@ -0,0 +1,79 @@
+import type { Ajv } from 'ajv';
+import { SchemaValidationError } from './errors.js';
+import { validate } from './validate.js';
+export interface ValidatorOptions {
+  inboundSchema?: object;
+  outboundSchema?: object;
+  envelope?: string;
+  formats?: Record<
+    string,
+    | string
+    | RegExp
+    | {
+        type?: 'string' | 'number';
+        validate: (data: string) => boolean;
+        async?: boolean;
+      }
+  >;
+  externalRefs?: object[];
+  ajv?: Ajv;
+}
+
+type AsyncMethod = (...args: unknown[]) => Promise<unknown>;
+
+export function validator(options: ValidatorOptions): MethodDecorator {
+  return (
+    _target,
+    _propertyKey,
+    descriptor: TypedPropertyDescriptor<AsyncMethod>
+  ) => {
+    if (!descriptor.value) {
+      return descriptor;
+    }
+
+    if (!options.inboundSchema && !options.outboundSchema) {
+      return descriptor;
+    }
+
+    const originalMethod = descriptor.value;
+
+    descriptor.value = async function (...args: unknown[]): Promise<unknown> {
+      let validatedInput = args[0];
+
+      if (options.inboundSchema) {
+        try {
+          validatedInput = validate({
+            payload: args[0],
+            schema: options.inboundSchema,
+            envelope: options.envelope,
+            formats: options.formats,
+            externalRefs: options.externalRefs,
+            ajv: options.ajv,
+          });
+        } catch (error) {
+          throw new SchemaValidationError('Inbound validation failed', error);
+        }
+      }
+
+      const result = await originalMethod.apply(this, [
+        validatedInput,
+        ...args.slice(1),
+      ]);
+      if (options.outboundSchema) {
+        try {
+          return validate({
+            payload: result,
+            schema: options.outboundSchema,
+            formats: options.formats,
+            externalRefs: options.externalRefs,
+            ajv: options.ajv,
+          });
+        } catch (error) {
+          throw new SchemaValidationError('Outbound Validation failed', error);
+        }
+      }
+      return result;
+    };
+    return descriptor;
+  };
+}
diff --git a/packages/validation/tests/unit/decorator.test.ts b/packages/validation/tests/unit/decorator.test.ts
new file mode 100644
index 0000000000..3af81ee76f
--- /dev/null
+++ b/packages/validation/tests/unit/decorator.test.ts
@@ -0,0 +1,90 @@
+import { describe, expect, it } from 'vitest';
+import { validator } from '../../src/decorator.js';
+import { SchemaValidationError } from '../../src/errors.js';
+
+const inboundSchema = {
+  type: 'object',
+  properties: {
+    value: { type: 'number' },
+  },
+  required: ['value'],
+  additionalProperties: false,
+};
+
+const outboundSchema = {
+  type: 'object',
+  properties: {
+    result: { type: 'number' },
+  },
+  required: ['result'],
+  additionalProperties: false,
+};
+
+class TestClass {
+  @validator({ inboundSchema, outboundSchema })
+  async multiply(input: { value: number }): Promise<{ result: number }> {
+    return { result: input.value * 2 };
+  }
+}
+
+describe('validator decorator', () => {
+  it('should validate inbound and outbound successfully', async () => {
+    // Prepare
+    const instance = new TestClass();
+    const input = { value: 5 };
+
+    // Act
+    const output = await instance.multiply(input);
+
+    // Assess
+    expect(output).toEqual({ result: 10 });
+  });
+
+  it('should throw error on inbound validation failure', async () => {
+    // Prepare
+    const instance = new TestClass();
+    const invalidInput = { value: 'not a number' } as unknown as {
+      value: number;
+    };
+
+    // Act & Assess
+    await expect(instance.multiply(invalidInput)).rejects.toThrow(
+      SchemaValidationError
+    );
+  });
+
+  it('should throw error on outbound validation failure', async () => {
+    // Prepare
+    class TestClassInvalid {
+      @validator({ inboundSchema, outboundSchema })
+      async multiply(input: { value: number }): Promise<{ result: number }> {
+        return { result: 'invalid' } as unknown as { result: number };
+      }
+    }
+    const instance = new TestClassInvalid();
+    const input = { value: 5 };
+
+    // Act & Assess
+    await expect(instance.multiply(input)).rejects.toThrow(
+      SchemaValidationError
+    );
+  });
+
+  it('should no-op when no schemas are provided', async () => {
+    // Prepare
+    class TestClassNoOp {
+      @validator({})
+      async echo(input: unknown): Promise<unknown> {
+        return input;
+      }
+    }
+    const instance = new TestClassNoOp();
+    const data = { foo: 'bar' };
+
+    // Act
+    const result = await instance.echo(data);
+
+    // Assess
+    expect(result).toEqual(data);
+  });
+});

From 4e0ea04414a747fb3f2adfa087937ca7716349ca Mon Sep 17 00:00:00 2001
From: Vatsal Goel <144617902+VatsalGoel3@users.noreply.github.com>
Date: Sat, 1 Mar 2025 00:36:53 -0700
Subject: [PATCH 2/6] Updated the test suite

---
 .../validation/tests/unit/decorator.test.ts   | 38 ++++++++++++-------
 1 file changed, 25 insertions(+), 13 deletions(-)

diff --git a/packages/validation/tests/unit/decorator.test.ts b/packages/validation/tests/unit/decorator.test.ts
index 3af81ee76f..127f1db1c8 100644
--- a/packages/validation/tests/unit/decorator.test.ts
+++ b/packages/validation/tests/unit/decorator.test.ts
@@ -20,33 +20,35 @@ const outboundSchema = {
   additionalProperties: false,
 };
 
-class TestClass {
-  @validator({ inboundSchema, outboundSchema })
-  async multiply(input: { value: number }): Promise<{ result: number }> {
-    return { result: input.value * 2 };
-  }
-}
-
 describe('validator decorator', () => {
   it('should validate inbound and outbound successfully', async () => {
     // Prepare
+    class TestClass {
+      @validator({ inboundSchema, outboundSchema })
+      async multiply(input: { value: number }): Promise<{ result: number }> {
+        return { result: input.value * 2 };
+      }
+    }
     const instance = new TestClass();
     const input = { value: 5 };
-
     // Act
     const output = await instance.multiply(input);
-
     // Assess
     expect(output).toEqual({ result: 10 });
   });
 
   it('should throw error on inbound validation failure', async () => {
     // Prepare
+    class TestClass {
+      @validator({ inboundSchema, outboundSchema })
+      async multiply(input: { value: number }): Promise<{ result: number }> {
+        return { result: input.value * 2 };
+      }
+    }
     const instance = new TestClass();
     const invalidInput = { value: 'not a number' } as unknown as {
       value: number;
     };
-
     // Act & Assess
     await expect(instance.multiply(invalidInput)).rejects.toThrow(
       SchemaValidationError
@@ -63,7 +65,6 @@ describe('validator decorator', () => {
     }
     const instance = new TestClassInvalid();
     const input = { value: 5 };
-
     // Act & Assess
     await expect(instance.multiply(input)).rejects.toThrow(
       SchemaValidationError
@@ -80,11 +81,22 @@ describe('validator decorator', () => {
     }
     const instance = new TestClassNoOp();
     const data = { foo: 'bar' };
-
     // Act
     const result = await instance.echo(data);
-
     // Assess
     expect(result).toEqual(data);
   });
+
+  it('should return descriptor unmodified if descriptor.value is undefined', () => {
+    // Prepare
+    const descriptor: PropertyDescriptor = {};
+    // Act
+    const result = validator({ inboundSchema })(
+      null as unknown,
+      'testMethod',
+      descriptor
+    );
+    // Assess
+    expect(result).toEqual(descriptor);
+  });
 });

From 4984867d3e8f94346b0362928fc6b064cdbc1ea4 Mon Sep 17 00:00:00 2001
From: Vatsal Goel <144617902+VatsalGoel3@users.noreply.github.com>
Date: Sat, 1 Mar 2025 00:39:08 -0700
Subject: [PATCH 3/6] updated test suite

---
 .../validation/tests/unit/decorator.test.ts   | 32 +++++++++++++++++++
 1 file changed, 32 insertions(+)

diff --git a/packages/validation/tests/unit/decorator.test.ts b/packages/validation/tests/unit/decorator.test.ts
index 127f1db1c8..8e0f5a06d3 100644
--- a/packages/validation/tests/unit/decorator.test.ts
+++ b/packages/validation/tests/unit/decorator.test.ts
@@ -99,4 +99,36 @@ describe('validator decorator', () => {
     // Assess
     expect(result).toEqual(descriptor);
   });
+
+  it('should validate inbound only', async () => {
+    // Prepare
+    class TestClassInbound {
+      @validator({ inboundSchema })
+      async process(input: { value: number }): Promise<{ data: string }> {
+        return { data: JSON.stringify(input) };
+      }
+    }
+    const instance = new TestClassInbound();
+    const input = { value: 10 };
+    // Act
+    const output = await instance.process(input);
+    // Assess
+    expect(output).toEqual({ data: JSON.stringify(input) });
+  });
+
+  it('should validate outbound only', async () => {
+    // Prepare
+    class TestClassOutbound {
+      @validator({ outboundSchema })
+      async process(input: { text: string }): Promise<{ result: number }> {
+        return { result: 42 };
+      }
+    }
+    const instance = new TestClassOutbound();
+    const input = { text: 'hello' };
+    // Act
+    const output = await instance.process(input);
+    // Assess
+    expect(output).toEqual({ result: 42 });
+  });
 });

From a4f1060838e218d4395eb2b3245c56aa31ff89fb Mon Sep 17 00:00:00 2001
From: Vatsal Goel <144617902+VatsalGoel3@users.noreply.github.com>
Date: Mon, 3 Mar 2025 08:42:10 -0700
Subject: [PATCH 4/6] refactor(validation): update decorator with improved
 types and schema validation

---
 packages/validation/src/decorator.ts          | 26 ++------------
 packages/validation/src/types.ts              | 36 ++++++++++++++-----
 .../validation/tests/unit/decorator.test.ts   |  2 +-
 3 files changed, 30 insertions(+), 34 deletions(-)

diff --git a/packages/validation/src/decorator.ts b/packages/validation/src/decorator.ts
index d92c8bf407..6232542ed7 100644
--- a/packages/validation/src/decorator.ts
+++ b/packages/validation/src/decorator.ts
@@ -1,23 +1,6 @@
-import type { Ajv } from 'ajv';
 import { SchemaValidationError } from './errors.js';
+import type { ValidatorOptions } from './types.js';
 import { validate } from './validate.js';
-export interface ValidatorOptions {
-  inboundSchema?: object;
-  outboundSchema?: object;
-  envelope?: string;
-  formats?: Record<
-    string,
-    | string
-    | RegExp
-    | {
-        type?: 'string' | 'number';
-        validate: (data: string) => boolean;
-        async?: boolean;
-      }
-  >;
-  externalRefs?: object[];
-  ajv?: Ajv;
-}
 
 type AsyncMethod = (...args: unknown[]) => Promise<unknown>;
 
@@ -30,16 +13,12 @@ export function validator(options: ValidatorOptions): MethodDecorator {
     if (!descriptor.value) {
       return descriptor;
     }
-
     if (!options.inboundSchema && !options.outboundSchema) {
       return descriptor;
     }
-
     const originalMethod = descriptor.value;
-
     descriptor.value = async function (...args: unknown[]): Promise<unknown> {
       let validatedInput = args[0];
-
       if (options.inboundSchema) {
         try {
           validatedInput = validate({
@@ -54,7 +33,6 @@ export function validator(options: ValidatorOptions): MethodDecorator {
           throw new SchemaValidationError('Inbound validation failed', error);
         }
       }
-
       const result = await originalMethod.apply(this, [
         validatedInput,
         ...args.slice(1),
@@ -69,7 +47,7 @@ export function validator(options: ValidatorOptions): MethodDecorator {
             ajv: options.ajv,
           });
         } catch (error) {
-          throw new SchemaValidationError('Outbound Validation failed', error);
+          throw new SchemaValidationError('Outbound validation failed', error);
         }
       }
       return result;
diff --git a/packages/validation/src/types.ts b/packages/validation/src/types.ts
index fd1efbaab0..4543e6ffe9 100644
--- a/packages/validation/src/types.ts
+++ b/packages/validation/src/types.ts
@@ -1,18 +1,36 @@
-import type Ajv from 'ajv';
-export interface ValidateParams<T = unknown> {
+import type {
+  Ajv,
+  AnySchema,
+  AsyncFormatDefinition,
+  FormatDefinition,
+} from 'ajv';
+
+type Prettify<T> = {
+  [K in keyof T]: T[K];
+} & {};
+
+type ValidateParams = {
   payload: unknown;
-  schema: object;
+  schema: AnySchema;
   envelope?: string;
   formats?: Record<
     string,
     | string
     | RegExp
-    | {
-        type?: 'string' | 'number';
-        validate: (data: string) => boolean;
-        async?: boolean;
-      }
+    | FormatDefinition<string>
+    | FormatDefinition<number>
+    | AsyncFormatDefinition<string>
+    | AsyncFormatDefinition<number>
   >;
   externalRefs?: object[];
   ajv?: Ajv;
-}
+};
+
+type ValidatorOptions = Prettify<
+  Omit<ValidateParams, 'payload' | 'schema'> & {
+    inboundSchema?: AnySchema;
+    outboundSchema?: AnySchema;
+  }
+>;
+
+export type { ValidateParams, ValidatorOptions };
diff --git a/packages/validation/tests/unit/decorator.test.ts b/packages/validation/tests/unit/decorator.test.ts
index 8e0f5a06d3..94ba2f0c40 100644
--- a/packages/validation/tests/unit/decorator.test.ts
+++ b/packages/validation/tests/unit/decorator.test.ts
@@ -92,7 +92,7 @@ describe('validator decorator', () => {
     const descriptor: PropertyDescriptor = {};
     // Act
     const result = validator({ inboundSchema })(
-      null as unknown,
+      null as unknown as object,
       'testMethod',
       descriptor
     );

From 8676b00a9bf39a06247d4a6b4c862c1d6346898e Mon Sep 17 00:00:00 2001
From: Andrea Amorosi <dreamorosi@gmail.com>
Date: Tue, 4 Mar 2025 09:36:01 +0100
Subject: [PATCH 5/6] Update packages/validation/src/decorator.ts

---
 packages/validation/src/decorator.ts | 51 +++++++++++++++-------------
 1 file changed, 28 insertions(+), 23 deletions(-)

diff --git a/packages/validation/src/decorator.ts b/packages/validation/src/decorator.ts
index 6232542ed7..b55e5159ad 100644
--- a/packages/validation/src/decorator.ts
+++ b/packages/validation/src/decorator.ts
@@ -1,33 +1,38 @@
 import { SchemaValidationError } from './errors.js';
-import type { ValidatorOptions } from './types.js';
 import { validate } from './validate.js';
-
-type AsyncMethod = (...args: unknown[]) => Promise<unknown>;
-
-export function validator(options: ValidatorOptions): MethodDecorator {
+import type { ValidatorOptions } from './types.js';
+export function validator(options: ValidatorOptions) {
   return (
-    _target,
-    _propertyKey,
-    descriptor: TypedPropertyDescriptor<AsyncMethod>
+    _target: unknown,
+    _propertyKey: string | symbol,
+    descriptor: PropertyDescriptor
   ) => {
     if (!descriptor.value) {
       return descriptor;
     }
-    if (!options.inboundSchema && !options.outboundSchema) {
+    const {
+      inboundSchema,
+      outboundSchema,
+      envelope,
+      formats,
+      externalRefs,
+      ajv,
+    } = options;
+    if (!inboundSchema && !outboundSchema) {
       return descriptor;
     }
     const originalMethod = descriptor.value;
-    descriptor.value = async function (...args: unknown[]): Promise<unknown> {
+    descriptor.value = async function (...args: unknown[]) {
       let validatedInput = args[0];
-      if (options.inboundSchema) {
+      if (inboundSchema) {
         try {
           validatedInput = validate({
-            payload: args[0],
-            schema: options.inboundSchema,
-            envelope: options.envelope,
-            formats: options.formats,
-            externalRefs: options.externalRefs,
-            ajv: options.ajv,
+            payload: validatedInput,
+            schema: inboundSchema,
+            envelope: envelope,
+            formats: formats,
+            externalRefs: externalRefs,
+            ajv: ajv,
           });
         } catch (error) {
           throw new SchemaValidationError('Inbound validation failed', error);
@@ -37,17 +42,17 @@ export function validator(options: ValidatorOptions): MethodDecorator {
         validatedInput,
         ...args.slice(1),
       ]);
-      if (options.outboundSchema) {
+      if (outboundSchema) {
         try {
           return validate({
             payload: result,
-            schema: options.outboundSchema,
-            formats: options.formats,
-            externalRefs: options.externalRefs,
-            ajv: options.ajv,
+            schema: outboundSchema,
+            formats: formats,
+            externalRefs: externalRefs,
+            ajv: ajv,
           });
         } catch (error) {
-          throw new SchemaValidationError('Outbound validation failed', error);
+          throw new SchemaValidationError('Outbound Validation failed', error);
         }
       }
       return result;

From e93f513870f8c717415c15237e10f8053459a9d2 Mon Sep 17 00:00:00 2001
From: Vatsal Goel <144617902+VatsalGoel3@users.noreply.github.com>
Date: Tue, 4 Mar 2025 01:47:56 -0700
Subject: [PATCH 6/6] Updated imports and exports

---
 packages/validation/src/index.ts    | 3 ++-
 packages/validation/src/validate.ts | 4 ++--
 2 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts
index 093ce43dd5..039a9236fa 100644
--- a/packages/validation/src/index.ts
+++ b/packages/validation/src/index.ts
@@ -1,2 +1,3 @@
-export { validate } from './validate';
+export { validate } from './validate.js';
 export { SchemaValidationError } from './errors.js';
+export { validator } from './decorator.js';
diff --git a/packages/validation/src/validate.ts b/packages/validation/src/validate.ts
index 75e2af96af..36d0fccb0a 100644
--- a/packages/validation/src/validate.ts
+++ b/packages/validation/src/validate.ts
@@ -1,9 +1,9 @@
 import { search } from '@aws-lambda-powertools/jmespath';
-import Ajv, { type ValidateFunction } from 'ajv';
+import { Ajv, type ValidateFunction } from 'ajv';
 import { SchemaValidationError } from './errors.js';
 import type { ValidateParams } from './types.js';
 
-export function validate<T = unknown>(params: ValidateParams<T>): T {
+export function validate<T = unknown>(params: ValidateParams): T {
   const { payload, schema, envelope, formats, externalRefs, ajv } = params;
   const ajvInstance = ajv || new Ajv({ allErrors: true });