Skip to content

Commit a668ad7

Browse files
authored
feat: OpenAPI support Zod Refine (#211)
1 parent ae515e8 commit a668ad7

3 files changed

Lines changed: 128 additions & 1 deletion

File tree

.changeset/stupid-donkeys-flash.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@ts-rest/open-api': patch
3+
---
4+
5+
Add missing support for Zod Refine

libs/ts-rest/open-api/src/lib/ts-rest-open-api.spec.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,5 +492,115 @@ describe('ts-rest-open-api', () => {
492492
)
493493
).toThrowError(/getPost/);
494494
});
495+
496+
it('works with zod refine', () => {
497+
const routerWithRefine = c.router({
498+
endpointWithZodRefine: {
499+
method: 'GET',
500+
path: '/refine',
501+
responses: {
502+
200: c.response<null>(),
503+
},
504+
query: z
505+
.object({
506+
foo: z.string(),
507+
})
508+
.refine((v) => v.foo === 'bar', {
509+
message: 'foo must be bar',
510+
}),
511+
},
512+
});
513+
514+
const schema = generateOpenApi(routerWithRefine, {
515+
info: { title: 'Blog API', version: '0.1' },
516+
});
517+
518+
expect(schema).toEqual({
519+
info: {
520+
title: 'Blog API',
521+
version: '0.1',
522+
},
523+
openapi: '3.0.2',
524+
paths: {
525+
'/refine': {
526+
get: {
527+
deprecated: undefined,
528+
description: undefined,
529+
parameters: [
530+
{
531+
in: 'query',
532+
name: 'foo',
533+
required: true,
534+
schema: {
535+
type: 'string',
536+
},
537+
},
538+
],
539+
responses: {
540+
'200': {
541+
description: '200',
542+
},
543+
},
544+
summary: undefined,
545+
tags: [],
546+
},
547+
},
548+
},
549+
});
550+
});
551+
552+
it('works with zod transform', () => {
553+
const routerWithTransform = c.router({
554+
endpointWithZodTransform: {
555+
method: 'GET',
556+
path: '/transform',
557+
responses: {
558+
200: c.response<null>(),
559+
},
560+
query: z
561+
.object({
562+
foo: z.string(),
563+
})
564+
.transform((v) => ({ fooTransformed: v.foo })),
565+
},
566+
});
567+
568+
const schema = generateOpenApi(routerWithTransform, {
569+
info: { title: 'Blog API', version: '0.1' },
570+
});
571+
572+
expect(schema).toEqual({
573+
info: {
574+
title: 'Blog API',
575+
version: '0.1',
576+
},
577+
openapi: '3.0.2',
578+
paths: {
579+
'/transform': {
580+
get: {
581+
deprecated: undefined,
582+
description: undefined,
583+
parameters: [
584+
{
585+
in: 'query',
586+
name: 'foo',
587+
required: true,
588+
schema: {
589+
type: 'string',
590+
},
591+
},
592+
],
593+
responses: {
594+
'200': {
595+
description: '200',
596+
},
597+
},
598+
summary: undefined,
599+
tags: [],
600+
},
601+
},
602+
},
603+
});
604+
});
495605
});
496606
});

libs/ts-rest/open-api/src/lib/ts-rest-open-api.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,19 @@ const getQueryParametersFromZod = (zodObject: unknown, jsonQuery = false) => {
9494
return [];
9595
}
9696

97-
return Object.entries(zodObject.shape).map(([key, value]) => {
97+
let zodShape;
98+
99+
if ('shape' in zodObject) {
100+
zodShape = zodObject.shape;
101+
// @ts-expect-error - Support ZodEffects
102+
} else if ('schema' in zodObject._def) {
103+
// @ts-expect-error - Support ZodEffects
104+
zodShape = zodObject._def.schema.shape;
105+
} else {
106+
throw new Error('Unknown zod object type');
107+
}
108+
109+
return Object.entries(zodShape).map(([key, value]) => {
98110
const schema = getOpenApiSchemaFromZod(value)!;
99111
const isObject = (value as z.ZodTypeAny)._def.typeName === 'ZodObject';
100112
const isRequired = !(value as z.ZodTypeAny).isOptional();

0 commit comments

Comments
 (0)