From 4174fe3dacefc0f7b5fd1ac1c5dfec6d50eb56d2 Mon Sep 17 00:00:00 2001 From: jlenon7 Date: Mon, 23 Mar 2026 13:03:36 -0300 Subject: [PATCH] feat: support zod transform on schemas --- package-lock.json | 4 +-- package.json | 2 +- src/kernels/HttpKernel.ts | 10 ++++-- src/router/RouteSchema.ts | 22 ++++++++++--- src/server/ServerImpl.ts | 2 +- tests/unit/router/RouteTest.ts | 58 ++++++++++++++++++++++++++++++++++ 6 files changed, 88 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 47da6c6..3bf5a6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@athenna/http", - "version": "5.51.0", + "version": "5.52.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@athenna/http", - "version": "5.51.0", + "version": "5.52.0", "license": "MIT", "devDependencies": { "@athenna/artisan": "^5.11.0", diff --git a/package.json b/package.json index b1d4f80..4d2da03 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@athenna/http", - "version": "5.51.0", + "version": "5.52.0", "description": "The Athenna Http server. Built on top of fastify.", "license": "MIT", "author": "João Lenon ", diff --git a/src/kernels/HttpKernel.ts b/src/kernels/HttpKernel.ts index 3c371af..16274c0 100644 --- a/src/kernels/HttpKernel.ts +++ b/src/kernels/HttpKernel.ts @@ -84,8 +84,14 @@ export class HttpKernel { if (swaggerPlugin) { const openapiConfig = Json.omit(Config.get('openapi', {}), ['paths']) - const pluginConfig = Json.omit(Config.get('http.swagger.configurations', {}), ['swagger']) - const swaggerConfig = Config.get('http.swagger.configurations.swagger', {}) + const pluginConfig = Json.omit( + Config.get('http.swagger.configurations', {}), + ['swagger'] + ) + const swaggerConfig = Config.get( + 'http.swagger.configurations.swagger', + {} + ) await Server.plugin(swaggerPlugin, { ...pluginConfig, diff --git a/src/router/RouteSchema.ts b/src/router/RouteSchema.ts index 4d926b6..3d4349d 100644 --- a/src/router/RouteSchema.ts +++ b/src/router/RouteSchema.ts @@ -145,17 +145,31 @@ async function parseSchema(schema: ZodAny, data: any) { } function toJsonSchema(schema: ZodAny, io: 'input' | 'output') { - const jsonSchemaMethod = - (schema as any)['~standard']?.jsonSchema?.[io] || - (schema as any).toJSONSchema + const standardJsonSchemaMethod = (schema as any)['~standard']?.jsonSchema?.[ + io + ] + + if (standardJsonSchemaMethod) { + const jsonSchema = standardJsonSchemaMethod({ + target: 'draft-07', + libraryOptions: { unrepresentable: 'any' } + }) + + delete jsonSchema.$schema + + return jsonSchema + } + + const jsonSchemaMethod = (schema as any).toJSONSchema if (!jsonSchemaMethod) { return {} } const jsonSchema = jsonSchemaMethod({ + io, target: 'draft-07', - libraryOptions: { unrepresentable: 'any' } + unrepresentable: 'any' }) delete jsonSchema.$schema diff --git a/src/server/ServerImpl.ts b/src/server/ServerImpl.ts index 2dba5f5..8c1da3f 100644 --- a/src/server/ServerImpl.ts +++ b/src/server/ServerImpl.ts @@ -403,7 +403,7 @@ export class ServerImpl extends Macroable { const normalizedSchema = normalizeRouteSchema(automaticSchema) const currentConfig = { ...(fastifyOptions.config || {}) } - + const currentSwaggerSchema = // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore diff --git a/tests/unit/router/RouteTest.ts b/tests/unit/router/RouteTest.ts index 45637cd..16e1413 100644 --- a/tests/unit/router/RouteTest.ts +++ b/tests/unit/router/RouteTest.ts @@ -143,6 +143,64 @@ export default class RouteTest { }) } + @Test() + public async shouldBeAbleToUseZodTransformSchemasWithoutBreakingSwagger({ assert }: Context) { + Route.post('transform', async ctx => { + await ctx.response.status(201).send({ + name: ctx.request.input('name') + }) + }).schema({ + body: z.object({ + name: z.string().transform(value => value.toUpperCase()) + }), + response: { + 201: z.object({ + name: z.string().transform(value => value.toUpperCase()) + }) + } + }) + + Route.register() + + const response = await Server.request({ + path: '/transform', + method: 'post', + payload: { name: 'lenon' } + }) + + assert.equal(response.statusCode, 201) + assert.deepEqual(response.json(), { name: 'LENON' }) + + const swagger = await Server.getSwagger() + + assert.containSubset(swagger.paths['/transform'], { + post: { + responses: { + '201': { + schema: { + type: 'object', + properties: { + name: {} + } + } + } + }, + parameters: [ + { + in: 'body', + schema: { + type: 'object', + properties: { + name: { type: 'string' } + }, + required: ['name'] + } + } + ] + } + }) + } + @Test() public async shouldBeAbleToUseExplicitZodCoercionForQuerystringAndParams({ assert }: Context) { Route.get('users/:id', async ctx => {