From 8ed99d9d791ed6bd8b897c705289b0464947e632 Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Sun, 27 Apr 2025 20:12:13 +0900 Subject: [PATCH] feat(zod-validator): add `validationFunction` option (#1140) Co-authored-by: migawka --- .changeset/tame-rooms-hide.md | 5 ++ packages/zod-validator/README.md | 24 +++++++ packages/zod-validator/src/index.test.ts | 82 ++++++++++++++++++++++++ packages/zod-validator/src/index.ts | 16 ++++- 4 files changed, 124 insertions(+), 3 deletions(-) create mode 100644 .changeset/tame-rooms-hide.md diff --git a/.changeset/tame-rooms-hide.md b/.changeset/tame-rooms-hide.md new file mode 100644 index 00000000..b0c0dd1e --- /dev/null +++ b/.changeset/tame-rooms-hide.md @@ -0,0 +1,5 @@ +--- +'@hono/zod-validator': minor +--- + +feat: add `validationFunction` option diff --git a/packages/zod-validator/README.md b/packages/zod-validator/README.md index 5a5f5acc..ddd7a782 100644 --- a/packages/zod-validator/README.md +++ b/packages/zod-validator/README.md @@ -68,6 +68,30 @@ app.post( ) ``` +### Custom validation function + +By default, this Validator validates values using `.safeParseAsync`. + +```ts +await schema.safeParseAsync(value) +``` + +But, if you want to use the [`.passthrough`](https://zod.dev/?id=passthrough), you can specify your own function in `validationFunction`. + +```ts +app.post( + '/', + zValidator('json', schema, undefined, { + validationFunction: async (schema, value) => { + return await schema.passthrough().safeParseAsync(value) + }, + }), + (c) => { + // ... + } +) +``` + ## Author Yusuke Wada diff --git a/packages/zod-validator/src/index.test.ts b/packages/zod-validator/src/index.test.ts index 23e21bd8..665d6899 100644 --- a/packages/zod-validator/src/index.test.ts +++ b/packages/zod-validator/src/index.test.ts @@ -378,3 +378,85 @@ describe('Case-Insensitive Headers', () => { type verify = Expect> }) }) + +describe('With options + validationFunction', () => { + const app = new Hono() + const jsonSchema = z.object({ + name: z.string(), + age: z.number(), + }) + + const route = app + .post('/', zValidator('json', jsonSchema), (c) => { + const data = c.req.valid('json') + return c.json({ + success: true, + data, + }) + }) + .post( + '/extended', + zValidator('json', jsonSchema, undefined, { + validationFunction: async (schema, value) => { + return await schema.passthrough().safeParseAsync(value) + }, + }), + (c) => { + const data = c.req.valid('json') + return c.json({ + success: true, + data, + }) + } + ) + + it('Should be ok due to passthrough schema', async () => { + const req = new Request('http://localhost/extended', { + body: JSON.stringify({ + name: 'Superman', + age: 20, + length: 170, + weight: 55, + }), + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }) + const res = await app.request(req) + expect(res).not.toBeNull() + expect(res.status).toBe(200) + expect(await res.json()).toEqual({ + success: true, + data: { + name: 'Superman', + age: 20, + length: 170, + weight: 55, + }, + }) + }) + it('Should be ok due to required schema', async () => { + const req = new Request('http://localhost', { + body: JSON.stringify({ + name: 'Superman', + age: 20, + }), + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }) + const res = await app.request(req) + + expect(res).not.toBeNull() + expect(res.status).toBe(200) + expect(await res.json()).toEqual({ + success: true, + data: { + name: 'Superman', + age: 20, + }, + }) + }) +}) diff --git a/packages/zod-validator/src/index.ts b/packages/zod-validator/src/index.ts index 9af06db8..80d3b38c 100644 --- a/packages/zod-validator/src/index.ts +++ b/packages/zod-validator/src/index.ts @@ -1,7 +1,7 @@ import type { Context, Env, Input, MiddlewareHandler, TypedResponse, ValidationTargets } from 'hono' import { validator } from 'hono/validator' import { ZodObject } from 'zod' -import type { ZodError, ZodSchema, z } from 'zod' +import type { SafeParseReturnType, ZodError, ZodSchema, z } from 'zod' export type Hook< T, @@ -43,7 +43,14 @@ export const zValidator = < >( target: Target, schema: T, - hook?: Hook, E, P, Target> + hook?: Hook, E, P, Target>, + options?: { + validationFunction: ( + schema: T, + value: ValidationTargets[Target] + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ) => SafeParseReturnType | Promise> + } ): MiddlewareHandler => // @ts-expect-error not typed well validator(target, async (value, c) => { @@ -63,7 +70,10 @@ export const zValidator = < ) } - const result = await schema.safeParseAsync(validatorValue) + const result = + options && options.validationFunction + ? await options.validationFunction(schema, validatorValue) + : await schema.safeParseAsync(validatorValue) if (hook) { const hookResult = await hook({ data: validatorValue, ...result, target }, c)