From 9769998550b1cb77f838827a9617b9dbbfa54537 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lubos=20=E2=80=8B?= Date: Thu, 12 Jun 2025 16:55:11 +0900 Subject: [PATCH] fix(zod): add metadata option --- .changeset/few-apples-yawn.md | 5 ++ docs/index.md | 2 +- docs/openapi-ts/get-started.md | 2 +- docs/openapi-ts/plugins/zod.md | 26 ++++++-- packages/openapi-ts-tests/test/3.1.x.test.ts | 14 +++++ .../3.1.x/validators-metadata/valibot.gen.ts | 59 +++++++++++++++++++ .../3.1.x/validators-metadata/zod.gen.ts | 59 +++++++++++++++++++ .../3.1.x/validators/valibot.gen.ts | 10 +++- .../__snapshots__/3.1.x/validators/zod.gen.ts | 10 +++- .../test/openapi-ts.config.ts | 12 ++-- .../test/spec/3.1.x/validators.yaml | 7 ++- packages/openapi-ts/src/plugins/zod/config.ts | 1 + packages/openapi-ts/src/plugins/zod/plugin.ts | 16 ++++- .../openapi-ts/src/plugins/zod/types.d.ts | 8 +++ 14 files changed, 208 insertions(+), 23 deletions(-) create mode 100644 .changeset/few-apples-yawn.md create mode 100644 packages/openapi-ts-tests/test/__snapshots__/3.1.x/validators-metadata/valibot.gen.ts create mode 100644 packages/openapi-ts-tests/test/__snapshots__/3.1.x/validators-metadata/zod.gen.ts diff --git a/.changeset/few-apples-yawn.md b/.changeset/few-apples-yawn.md new file mode 100644 index 000000000..83eb6e2b0 --- /dev/null +++ b/.changeset/few-apples-yawn.md @@ -0,0 +1,5 @@ +--- +'@hey-api/openapi-ts': patch +--- + +fix(zod): add `metadata` option to generate additional metadata for documentation, code generation, AI structured outputs, form validation, and other purposes diff --git a/docs/index.md b/docs/index.md index 347a2cedd..af27e9f30 100644 --- a/docs/index.md +++ b/docs/index.md @@ -3,7 +3,7 @@ layout: home hero: name: High-quality tools for interacting with APIs - tagline: Codegen for your TypeScript projects. Trusted over 1,250,000 times each month to generate reliable API clients and SDKs. + tagline: Codegen for your TypeScript projects. Trusted over 1,500,000 times each month to generate reliable API clients and SDKs. actions: - link: /openapi-ts/get-started text: Get Started diff --git a/docs/openapi-ts/get-started.md b/docs/openapi-ts/get-started.md index a76086076..9b11308d9 100644 --- a/docs/openapi-ts/get-started.md +++ b/docs/openapi-ts/get-started.md @@ -13,7 +13,7 @@ import { embedProject } from '../embed' This package is in initial development. The interface might change before it becomes stable. We encourage you to leave feedback on [GitHub](https://github.com/hey-api/openapi-ts/issues). ::: -[@hey-api/openapi-ts](https://github.com/hey-api/openapi-ts) is an OpenAPI to TypeScript codegen trusted over 1,250,000 times each month to generate reliable API clients and SDKs. The code is [MIT-licensed](/license) and free to use. Discover available features below or view our [roadmap](https://github.com/orgs/hey-api/discussions/1495) to learn what's coming next. +[@hey-api/openapi-ts](https://github.com/hey-api/openapi-ts) is an OpenAPI to TypeScript codegen trusted over 1,500,000 times each month to generate reliable API clients and SDKs. The code is [MIT-licensed](/license) and free to use. Discover available features below or view our [roadmap](https://github.com/orgs/hey-api/discussions/1495) to learn what's coming next. ### Demo diff --git a/docs/openapi-ts/plugins/zod.md b/docs/openapi-ts/plugins/zod.md index 0e72f11cf..56dd256d6 100644 --- a/docs/openapi-ts/plugins/zod.md +++ b/docs/openapi-ts/plugins/zod.md @@ -33,13 +33,11 @@ Launch demo In your [configuration](/openapi-ts/get-started), add `zod` to your plugins and you'll be ready to generate Zod artifacts. :tada: ```js -import { defaultPlugins } from '@hey-api/openapi-ts'; - export default { input: 'https://get.heyapi.dev/hey-api/backend', output: 'src/client', plugins: [ - ...defaultPlugins, + // ...other plugins '@hey-api/client-fetch', 'zod', // [!code ++] ], @@ -51,13 +49,11 @@ export default { To automatically validate response data in your SDKs, set `sdk.validator` to `true`. ```js -import { defaultPlugins } from '@hey-api/openapi-ts'; - export default { input: 'https://get.heyapi.dev/hey-api/backend', output: 'src/client', plugins: [ - ...defaultPlugins, + // ...other plugins '@hey-api/client-fetch', 'zod', { @@ -120,5 +116,23 @@ const zBar = z.object({ }); ``` +## Metadata + +It's often useful to associate a schema with some additional metadata for documentation, code generation, AI structured outputs, form validation, and other purposes. If this is your use case, you can set `metadata` to `true` to generate additional metadata about schemas. + +```js +export default { + input: 'https://get.heyapi.dev/hey-api/backend', + output: 'src/client', + plugins: [ + // ...other plugins + { + metadata: true, // [!code ++] + name: 'zod', + }, + ], +}; +``` + diff --git a/packages/openapi-ts-tests/test/3.1.x.test.ts b/packages/openapi-ts-tests/test/3.1.x.test.ts index 1fa5cc53e..fafb4918a 100644 --- a/packages/openapi-ts-tests/test/3.1.x.test.ts +++ b/packages/openapi-ts-tests/test/3.1.x.test.ts @@ -717,6 +717,20 @@ describe(`OpenAPI ${version}`, () => { }), description: 'generates validator schemas', }, + { + config: createConfig({ + input: 'validators.yaml', + output: 'validators-metadata', + plugins: [ + 'valibot', + { + metadata: true, + name: 'zod', + }, + ], + }), + description: 'generates validator schemas with metadata', + }, { config: createConfig({ input: 'validators-bigint-min-max.json', diff --git a/packages/openapi-ts-tests/test/__snapshots__/3.1.x/validators-metadata/valibot.gen.ts b/packages/openapi-ts-tests/test/__snapshots__/3.1.x/validators-metadata/valibot.gen.ts new file mode 100644 index 000000000..735206c81 --- /dev/null +++ b/packages/openapi-ts-tests/test/__snapshots__/3.1.x/validators-metadata/valibot.gen.ts @@ -0,0 +1,59 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import * as v from 'valibot'; + +/** + * This is Bar schema. + */ +export const vBar: v.GenericSchema = v.object({ + foo: v.optional(v.lazy(() => { + return vFoo; + })) +}); + +/** + * This is Foo schema. + */ +export const vFoo: v.GenericSchema = v.optional(v.union([ + v.object({ + foo: v.optional(v.pipe(v.string(), v.regex(/^\d{3}-\d{2}-\d{4}$/))), + bar: v.optional(vBar), + baz: v.optional(v.array(v.lazy(() => { + return vFoo; + }))), + qux: v.optional(v.pipe(v.number(), v.integer(), v.gtValue(0)), 0) + }), + v.null() +]), null); + +export const vBaz = v.optional(v.pipe(v.pipe(v.string(), v.regex(/foo\nbar/)), v.readonly()), 'baz'); + +/** + * This is Foo parameter. + */ +export const vFoo2 = v.string(); + +export const vFoo3 = v.object({ + foo: v.optional(vBar) +}); + +export const vPatchFooData = v.object({ + foo: v.optional(v.string()) +}); + +/** + * This is Foo parameter. + */ +export const vPatchFooParameterFoo = v.string(); + +export const vPatchFooParameterBar = vBar; + +export const vPatchFooParameterBaz = v.object({ + baz: v.optional(v.string()) +}); + +export const vPatchFooParameterQux = v.pipe(v.string(), v.isoDate()); + +export const vPatchFooParameterQuux = v.pipe(v.string(), v.isoDateTime()); + +export const vPostFooData = vFoo3; \ No newline at end of file diff --git a/packages/openapi-ts-tests/test/__snapshots__/3.1.x/validators-metadata/zod.gen.ts b/packages/openapi-ts-tests/test/__snapshots__/3.1.x/validators-metadata/zod.gen.ts new file mode 100644 index 000000000..534fb7d7c --- /dev/null +++ b/packages/openapi-ts-tests/test/__snapshots__/3.1.x/validators-metadata/zod.gen.ts @@ -0,0 +1,59 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { z } from 'zod'; + +/** + * This is Bar schema. + */ +export const zBar: z.AnyZodObject = z.object({ + foo: z.lazy(() => { + return zFoo; + }).optional() +}).describe('This is Bar schema.'); + +/** + * This is Foo schema. + */ +export const zFoo: z.ZodTypeAny = z.union([ + z.object({ + foo: z.string().regex(/^\d{3}-\d{2}-\d{4}$/).describe('This is foo property.').optional(), + bar: zBar.optional(), + baz: z.array(z.lazy(() => { + return zFoo; + })).describe('This is baz property.').optional(), + qux: z.number().int().gt(0).describe('This is qux property.').optional().default(0) + }), + z.null() +]).default(null); + +export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); + +/** + * This is Foo parameter. + */ +export const zFoo2 = z.string().describe('This is Foo parameter.'); + +export const zFoo3 = z.object({ + foo: zBar.optional() +}); + +export const zPatchFooData = z.object({ + foo: z.string().optional() +}); + +/** + * This is Foo parameter. + */ +export const zPatchFooParameterFoo = z.string().describe('This is Foo parameter.'); + +export const zPatchFooParameterBar = zBar; + +export const zPatchFooParameterBaz = z.object({ + baz: z.string().optional() +}); + +export const zPatchFooParameterQux = z.string().date(); + +export const zPatchFooParameterQuux = z.string().datetime(); + +export const zPostFooData = zFoo3; \ No newline at end of file diff --git a/packages/openapi-ts-tests/test/__snapshots__/3.1.x/validators/valibot.gen.ts b/packages/openapi-ts-tests/test/__snapshots__/3.1.x/validators/valibot.gen.ts index 849c39be0..735206c81 100644 --- a/packages/openapi-ts-tests/test/__snapshots__/3.1.x/validators/valibot.gen.ts +++ b/packages/openapi-ts-tests/test/__snapshots__/3.1.x/validators/valibot.gen.ts @@ -2,12 +2,18 @@ import * as v from 'valibot'; +/** + * This is Bar schema. + */ export const vBar: v.GenericSchema = v.object({ foo: v.optional(v.lazy(() => { return vFoo; })) }); +/** + * This is Foo schema. + */ export const vFoo: v.GenericSchema = v.optional(v.union([ v.object({ foo: v.optional(v.pipe(v.string(), v.regex(/^\d{3}-\d{2}-\d{4}$/))), @@ -23,7 +29,7 @@ export const vFoo: v.GenericSchema = v.optional(v.union([ export const vBaz = v.optional(v.pipe(v.pipe(v.string(), v.regex(/foo\nbar/)), v.readonly()), 'baz'); /** - * aaaaa + * This is Foo parameter. */ export const vFoo2 = v.string(); @@ -36,7 +42,7 @@ export const vPatchFooData = v.object({ }); /** - * aaaaa + * This is Foo parameter. */ export const vPatchFooParameterFoo = v.string(); diff --git a/packages/openapi-ts-tests/test/__snapshots__/3.1.x/validators/zod.gen.ts b/packages/openapi-ts-tests/test/__snapshots__/3.1.x/validators/zod.gen.ts index dda3b26f9..424948dc6 100644 --- a/packages/openapi-ts-tests/test/__snapshots__/3.1.x/validators/zod.gen.ts +++ b/packages/openapi-ts-tests/test/__snapshots__/3.1.x/validators/zod.gen.ts @@ -2,12 +2,18 @@ import { z } from 'zod'; +/** + * This is Bar schema. + */ export const zBar: z.AnyZodObject = z.object({ foo: z.lazy(() => { return zFoo; }).optional() }); +/** + * This is Foo schema. + */ export const zFoo: z.ZodTypeAny = z.union([ z.object({ foo: z.string().regex(/^\d{3}-\d{2}-\d{4}$/).optional(), @@ -23,7 +29,7 @@ export const zFoo: z.ZodTypeAny = z.union([ export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); /** - * aaaaa + * This is Foo parameter. */ export const zFoo2 = z.string(); @@ -36,7 +42,7 @@ export const zPatchFooData = z.object({ }); /** - * aaaaa + * This is Foo parameter. */ export const zPatchFooParameterFoo = z.string(); diff --git a/packages/openapi-ts-tests/test/openapi-ts.config.ts b/packages/openapi-ts-tests/test/openapi-ts.config.ts index 2cb8e688c..c8cfeb76e 100644 --- a/packages/openapi-ts-tests/test/openapi-ts.config.ts +++ b/packages/openapi-ts-tests/test/openapi-ts.config.ts @@ -51,12 +51,7 @@ export default defineConfig(() => { // 'invalid', // 'servers-entry.yaml', // ), - path: path.resolve( - __dirname, - 'spec', - '3.1.x', - 'object-property-names.yaml', - ), + path: path.resolve(__dirname, 'spec', '3.1.x', 'validators.yaml'), // path: 'http://localhost:4000/', // path: 'https://get.heyapi.dev/', // path: 'https://get.heyapi.dev/hey-api/backend?branch=main&version=1.0.0', @@ -150,12 +145,13 @@ export default defineConfig(() => { { // comments: false, // exportFromIndex: true, - // name: 'valibot', + name: 'valibot', }, { // comments: false, // exportFromIndex: true, - // name: 'zod', + metadata: true, + name: 'zod', }, ], // useOptions: false, diff --git a/packages/openapi-ts-tests/test/spec/3.1.x/validators.yaml b/packages/openapi-ts-tests/test/spec/3.1.x/validators.yaml index d540837ee..d6ac2b53d 100644 --- a/packages/openapi-ts-tests/test/spec/3.1.x/validators.yaml +++ b/packages/openapi-ts-tests/test/spec/3.1.x/validators.yaml @@ -53,7 +53,7 @@ paths: components: parameters: Foo: - description: aaaaa + description: 'This is Foo parameter.' name: foo in: query required: false @@ -72,17 +72,21 @@ components: schemas: Foo: default: null + description: 'This is Foo schema.' properties: foo: + description: 'This is foo property.' pattern: ^\d{3}-\d{2}-\d{4}$ type: string bar: $ref: '#/components/schemas/Bar' baz: + description: 'This is baz property.' items: $ref: '#/components/schemas/Foo' type: array qux: + description: 'This is qux property.' default: 0 exclusiveMinimum: 0 type: integer @@ -90,6 +94,7 @@ components: - object - 'null' Bar: + description: 'This is Bar schema.' properties: foo: $ref: '#/components/schemas/Foo' diff --git a/packages/openapi-ts/src/plugins/zod/config.ts b/packages/openapi-ts/src/plugins/zod/config.ts index a36617fca..25c188945 100644 --- a/packages/openapi-ts/src/plugins/zod/config.ts +++ b/packages/openapi-ts/src/plugins/zod/config.ts @@ -8,6 +8,7 @@ export const defaultConfig: Plugin.Config = { _tags: ['validator'], comments: true, exportFromIndex: false, + metadata: false, name: 'zod', output: 'zod', }; diff --git a/packages/openapi-ts/src/plugins/zod/plugin.ts b/packages/openapi-ts/src/plugins/zod/plugin.ts index b689481fe..674624b79 100644 --- a/packages/openapi-ts/src/plugins/zod/plugin.ts +++ b/packages/openapi-ts/src/plugins/zod/plugin.ts @@ -24,8 +24,10 @@ export const zodId = 'zod'; // frequently used identifiers const andIdentifier = compiler.identifier({ text: 'and' }); +const arrayIdentifier = compiler.identifier({ text: 'array' }); const coerceIdentifier = compiler.identifier({ text: 'coerce' }); const defaultIdentifier = compiler.identifier({ text: 'default' }); +const describeIdentifier = compiler.identifier({ text: 'describe' }); const intersectionIdentifier = compiler.identifier({ text: 'intersection' }); const lazyIdentifier = compiler.identifier({ text: 'lazy' }); const lengthIdentifier = compiler.identifier({ text: 'length' }); @@ -54,7 +56,7 @@ const arrayTypeToZodSchema = ({ }): ts.CallExpression => { const functionName = compiler.propertyAccessExpression({ expression: zIdentifier, - name: compiler.identifier({ text: 'array' }), + name: arrayIdentifier, }); let arrayExpression: ts.CallExpression | undefined; @@ -100,7 +102,7 @@ const arrayTypeToZodSchema = ({ arrayExpression = compiler.callExpression({ functionName: compiler.propertyAccessExpression({ expression: zIdentifier, - name: compiler.identifier({ text: 'array' }), + name: arrayIdentifier, }), parameters: [ compiler.callExpression({ @@ -987,6 +989,16 @@ const schemaToZodSchema = ({ }); anyType = zodSchema.anyType; expression = zodSchema.expression; + + if (plugin.metadata && schema.description) { + expression = compiler.callExpression({ + functionName: compiler.propertyAccessExpression({ + expression, + name: describeIdentifier, + }), + parameters: [compiler.stringLiteral({ text: schema.description })], + }); + } } else if (schema.items) { schema = deduplicateSchema({ schema }); diff --git a/packages/openapi-ts/src/plugins/zod/types.d.ts b/packages/openapi-ts/src/plugins/zod/types.d.ts index 2db06a13e..b7464dbbc 100644 --- a/packages/openapi-ts/src/plugins/zod/types.d.ts +++ b/packages/openapi-ts/src/plugins/zod/types.d.ts @@ -15,6 +15,14 @@ export interface Config extends Plugin.Name<'zod'> { * @default false */ exportFromIndex?: boolean; + /** + * Enable Zod metadata support? It's often useful to associate a schema with + * some additional metadata for documentation, code generation, AI + * structured outputs, form validation, and other purposes. + * + * @default false + */ + metadata?: boolean; /** * Customise the Zod schema name. By default, `z{{name}}` is used, * where `name` is a definition name or an operation name.