fork of hey-api/openapi-ts because I need some additional things
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #3497 from hey-api/copilot/expand-metadata-option-zod-valibot

Expand `metadata` option for Zod and Valibot to support builder functions

authored by

Lubos and committed by
GitHub
3ae2b778 d2c37051

+376 -43
+5
.changeset/soft-comics-walk.md
··· 1 + --- 2 + "@hey-api/openapi-ts": patch 3 + --- 4 + 5 + **plugin(zod)**: support function in `metadata` option
+5
.changeset/soft-comics-walks.md
··· 1 + --- 2 + "@hey-api/openapi-ts": patch 3 + --- 4 + 5 + **plugin(valibot)**: support function in `metadata` option
+17
packages/openapi-ts-tests/main/test/3.1.x.test.ts
··· 946 946 { 947 947 config: createConfig({ 948 948 input: 'validators.yaml', 949 + output: 'validators-metadata-fn', 950 + plugins: [ 951 + { 952 + metadata: ({ $, node, schema }) => { 953 + node 954 + .prop('custom', $.literal('value')) 955 + .prop('title', $.literal(schema.description ?? schema.type ?? '')); 956 + }, 957 + name: 'valibot', 958 + }, 959 + ], 960 + }), 961 + description: 'generates validator schemas with metadata function', 962 + }, 963 + { 964 + config: createConfig({ 965 + input: 'validators.yaml', 949 966 output: 'validators-types', 950 967 plugins: ['valibot'], 951 968 }),
+57
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-metadata-fn/valibot.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import * as v from 'valibot'; 4 + 5 + export const vBaz = v.optional(v.pipe(v.pipe(v.string(), v.regex(/foo\nbar/)), v.metadata({ custom: 'value', title: 'string' }), v.readonly()), 'baz'); 6 + 7 + export const vQux = v.pipe(v.record(v.string(), v.pipe(v.object({ 8 + qux: v.optional(v.pipe(v.string(), v.metadata({ custom: 'value', title: 'string' }))) 9 + }), v.metadata({ custom: 'value', title: 'object' }))), v.metadata({ custom: 'value', title: 'object' })); 10 + 11 + /** 12 + * This is Foo schema. 13 + */ 14 + export const vFoo: v.GenericSchema = v.nullish(v.pipe(v.object({ 15 + foo: v.optional(v.pipe(v.pipe(v.string(), v.regex(/^\d{3}-\d{2}-\d{4}$/)), v.metadata({ custom: 'value', title: 'This is foo property.' }))), 16 + bar: v.optional(v.lazy(() => vBar)), 17 + baz: v.optional(v.pipe(v.array(v.lazy(() => vFoo)), v.metadata({ custom: 'value', title: 'This is baz property.' }))), 18 + qux: v.optional(v.pipe(v.pipe(v.number(), v.integer(), v.gtValue(0)), v.metadata({ custom: 'value', title: 'This is qux property.' })), 0) 19 + }), v.metadata({ custom: 'value', title: 'object' })), null); 20 + 21 + /** 22 + * This is Bar schema. 23 + */ 24 + export const vBar = v.pipe(v.object({ 25 + foo: v.optional(vFoo) 26 + }), v.metadata({ custom: 'value', title: 'This is Bar schema.' })); 27 + 28 + /** 29 + * This is Foo parameter. 30 + */ 31 + export const vFoo2 = v.pipe(v.string(), v.metadata({ custom: 'value', title: 'This is Foo parameter.' })); 32 + 33 + export const vFoo3 = v.pipe(v.object({ 34 + foo: v.optional(vBar) 35 + }), v.metadata({ custom: 'value', title: 'object' })); 36 + 37 + export const vPatchFooData = v.pipe(v.object({ 38 + body: v.pipe(v.object({ 39 + foo: v.optional(v.pipe(v.string(), v.metadata({ custom: 'value', title: 'string' }))) 40 + }), v.metadata({ custom: 'value', title: 'object' })), 41 + path: v.optional(v.pipe(v.never(), v.metadata({ custom: 'value', title: 'never' }))), 42 + query: v.optional(v.pipe(v.object({ 43 + foo: v.optional(v.pipe(v.string(), v.metadata({ custom: 'value', title: 'This is Foo parameter.' }))), 44 + bar: v.optional(vBar), 45 + baz: v.optional(v.pipe(v.object({ 46 + baz: v.optional(v.pipe(v.string(), v.metadata({ custom: 'value', title: 'string' }))) 47 + }), v.metadata({ custom: 'value', title: 'object' }))), 48 + qux: v.optional(v.pipe(v.pipe(v.string(), v.isoDate()), v.metadata({ custom: 'value', title: 'string' }))), 49 + quux: v.optional(v.pipe(v.pipe(v.string(), v.isoTimestamp()), v.metadata({ custom: 'value', title: 'string' }))) 50 + }), v.metadata({ custom: 'value', title: 'object' }))) 51 + }), v.metadata({ custom: 'value', title: 'object' })); 52 + 53 + export const vPostFooData = v.pipe(v.object({ 54 + body: vFoo3, 55 + path: v.optional(v.pipe(v.never(), v.metadata({ custom: 'value', title: 'never' }))), 56 + query: v.optional(v.pipe(v.never(), v.metadata({ custom: 'value', title: 'never' }))) 57 + }), v.metadata({ custom: 'value', title: 'object' }));
+57
packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/validators-metadata-fn/zod.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import * as z from 'zod/mini'; 4 + 5 + export const zBaz = z._default(z.readonly(z.string().check(z.regex(/foo\nbar/)).register(z.globalRegistry, { custom: 'value', title: 'string' })), 'baz'); 6 + 7 + export const zQux = z.record(z.string(), z.object({ 8 + qux: z.optional(z.string().register(z.globalRegistry, { custom: 'value', title: 'string' })) 9 + }).register(z.globalRegistry, { custom: 'value', title: 'object' })).register(z.globalRegistry, { custom: 'value', title: 'object' }); 10 + 11 + /** 12 + * This is Foo schema. 13 + */ 14 + export const zFoo = z._default(z.nullable(z.object({ 15 + foo: z.optional(z.string().check(z.regex(/^\d{3}-\d{2}-\d{4}$/)).register(z.globalRegistry, { custom: 'value', title: 'This is foo property.' })), 16 + bar: z.optional(z.lazy((): any => zBar)), 17 + baz: z.optional(z.array(z.lazy((): any => zFoo)).register(z.globalRegistry, { custom: 'value', title: 'This is baz property.' })), 18 + qux: z._default(z.optional(z.int().check(z.gt(0)).register(z.globalRegistry, { custom: 'value', title: 'This is qux property.' })), 0) 19 + }).register(z.globalRegistry, { custom: 'value', title: 'object' })), null); 20 + 21 + /** 22 + * This is Bar schema. 23 + */ 24 + export const zBar = z.object({ 25 + foo: z.optional(zFoo) 26 + }).register(z.globalRegistry, { custom: 'value', title: 'This is Bar schema.' }); 27 + 28 + /** 29 + * This is Foo parameter. 30 + */ 31 + export const zFoo2 = z.string().register(z.globalRegistry, { custom: 'value', title: 'This is Foo parameter.' }); 32 + 33 + export const zFoo3 = z.object({ 34 + foo: z.optional(zBar) 35 + }).register(z.globalRegistry, { custom: 'value', title: 'object' }); 36 + 37 + export const zPatchFooData = z.object({ 38 + body: z.object({ 39 + foo: z.optional(z.string().register(z.globalRegistry, { custom: 'value', title: 'string' })) 40 + }).register(z.globalRegistry, { custom: 'value', title: 'object' }), 41 + path: z.optional(z.never().register(z.globalRegistry, { custom: 'value', title: 'never' })), 42 + query: z.optional(z.object({ 43 + foo: z.optional(z.string().register(z.globalRegistry, { custom: 'value', title: 'This is Foo parameter.' })), 44 + bar: z.optional(zBar), 45 + baz: z.optional(z.object({ 46 + baz: z.optional(z.string().register(z.globalRegistry, { custom: 'value', title: 'string' })) 47 + }).register(z.globalRegistry, { custom: 'value', title: 'object' })), 48 + qux: z.optional(z.iso.date().register(z.globalRegistry, { custom: 'value', title: 'string' })), 49 + quux: z.optional(z.iso.datetime().register(z.globalRegistry, { custom: 'value', title: 'string' })) 50 + }).register(z.globalRegistry, { custom: 'value', title: 'object' })) 51 + }).register(z.globalRegistry, { custom: 'value', title: 'object' }); 52 + 53 + export const zPostFooData = z.object({ 54 + body: zFoo3, 55 + path: z.optional(z.never().register(z.globalRegistry, { custom: 'value', title: 'never' })), 56 + query: z.optional(z.never().register(z.globalRegistry, { custom: 'value', title: 'never' })) 57 + }).register(z.globalRegistry, { custom: 'value', title: 'object' });
+57
packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/validators-metadata-fn/zod.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import { z } from 'zod/v3'; 4 + 5 + export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); 6 + 7 + export const zQux = z.record(z.object({ 8 + qux: z.string().optional() 9 + })); 10 + 11 + /** 12 + * This is Foo schema. 13 + */ 14 + export const zFoo: z.ZodTypeAny = z.object({ 15 + foo: z.string().regex(/^\d{3}-\d{2}-\d{4}$/).describe('This is foo property.').optional(), 16 + bar: z.lazy(() => zBar).optional(), 17 + baz: z.array(z.lazy(() => zFoo)).describe('This is baz property.').optional(), 18 + qux: z.number().int().gt(0).describe('This is qux property.').optional().default(0) 19 + }).nullable().default(null); 20 + 21 + /** 22 + * This is Bar schema. 23 + */ 24 + export const zBar = z.object({ 25 + foo: zFoo.optional() 26 + }).describe('This is Bar schema.'); 27 + 28 + /** 29 + * This is Foo parameter. 30 + */ 31 + export const zFoo2 = z.string().describe('This is Foo parameter.'); 32 + 33 + export const zFoo3 = z.object({ 34 + foo: zBar.optional() 35 + }); 36 + 37 + export const zPatchFooData = z.object({ 38 + body: z.object({ 39 + foo: z.string().optional() 40 + }), 41 + path: z.never().optional(), 42 + query: z.object({ 43 + foo: z.string().describe('This is Foo parameter.').optional(), 44 + bar: zBar.optional(), 45 + baz: z.object({ 46 + baz: z.string().optional() 47 + }).optional(), 48 + qux: z.string().date().optional(), 49 + quux: z.string().datetime().optional() 50 + }).optional() 51 + }); 52 + 53 + export const zPostFooData = z.object({ 54 + body: zFoo3, 55 + path: z.never().optional(), 56 + query: z.never().optional() 57 + });
+57
packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/validators-metadata-fn/zod.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import * as z from 'zod'; 4 + 5 + export const zBaz = z.string().regex(/foo\nbar/).register(z.globalRegistry, { custom: 'value', title: 'string' }).readonly().default('baz'); 6 + 7 + export const zQux = z.record(z.string(), z.object({ 8 + qux: z.string().register(z.globalRegistry, { custom: 'value', title: 'string' }).optional() 9 + }).register(z.globalRegistry, { custom: 'value', title: 'object' })).register(z.globalRegistry, { custom: 'value', title: 'object' }); 10 + 11 + /** 12 + * This is Foo schema. 13 + */ 14 + export const zFoo = z.object({ 15 + foo: z.string().regex(/^\d{3}-\d{2}-\d{4}$/).register(z.globalRegistry, { custom: 'value', title: 'This is foo property.' }).optional(), 16 + bar: z.lazy((): any => zBar).optional(), 17 + baz: z.array(z.lazy((): any => zFoo)).register(z.globalRegistry, { custom: 'value', title: 'This is baz property.' }).optional(), 18 + qux: z.int().gt(0).register(z.globalRegistry, { custom: 'value', title: 'This is qux property.' }).optional().default(0) 19 + }).register(z.globalRegistry, { custom: 'value', title: 'object' }).nullable().default(null); 20 + 21 + /** 22 + * This is Bar schema. 23 + */ 24 + export const zBar = z.object({ 25 + foo: zFoo.optional() 26 + }).register(z.globalRegistry, { custom: 'value', title: 'This is Bar schema.' }); 27 + 28 + /** 29 + * This is Foo parameter. 30 + */ 31 + export const zFoo2 = z.string().register(z.globalRegistry, { custom: 'value', title: 'This is Foo parameter.' }); 32 + 33 + export const zFoo3 = z.object({ 34 + foo: zBar.optional() 35 + }).register(z.globalRegistry, { custom: 'value', title: 'object' }); 36 + 37 + export const zPatchFooData = z.object({ 38 + body: z.object({ 39 + foo: z.string().register(z.globalRegistry, { custom: 'value', title: 'string' }).optional() 40 + }).register(z.globalRegistry, { custom: 'value', title: 'object' }), 41 + path: z.never().register(z.globalRegistry, { custom: 'value', title: 'never' }).optional(), 42 + query: z.object({ 43 + foo: z.string().register(z.globalRegistry, { custom: 'value', title: 'This is Foo parameter.' }).optional(), 44 + bar: zBar.optional(), 45 + baz: z.object({ 46 + baz: z.string().register(z.globalRegistry, { custom: 'value', title: 'string' }).optional() 47 + }).register(z.globalRegistry, { custom: 'value', title: 'object' }).optional(), 48 + qux: z.iso.date().register(z.globalRegistry, { custom: 'value', title: 'string' }).optional(), 49 + quux: z.iso.datetime().register(z.globalRegistry, { custom: 'value', title: 'string' }).optional() 50 + }).register(z.globalRegistry, { custom: 'value', title: 'object' }).optional() 51 + }).register(z.globalRegistry, { custom: 'value', title: 'object' }); 52 + 53 + export const zPostFooData = z.object({ 54 + body: zFoo3, 55 + path: z.never().register(z.globalRegistry, { custom: 'value', title: 'never' }).optional(), 56 + query: z.never().register(z.globalRegistry, { custom: 'value', title: 'never' }).optional() 57 + }).register(z.globalRegistry, { custom: 'value', title: 'object' });
+18
packages/openapi-ts-tests/zod/v4/test/3.1.x.test.ts
··· 88 88 { 89 89 config: createConfig({ 90 90 input: 'validators.yaml', 91 + output: 'validators-metadata-fn', 92 + plugins: [ 93 + { 94 + compatibilityVersion: zodVersion.compatibilityVersion, 95 + metadata: ({ $, node, schema }) => { 96 + node 97 + .prop('custom', $.literal('value')) 98 + .prop('title', $.literal(schema.description ?? schema.type ?? '')); 99 + }, 100 + name: 'zod', 101 + }, 102 + ], 103 + }), 104 + description: 'generates validator schemas with metadata function', 105 + }, 106 + { 107 + config: createConfig({ 108 + input: 'validators.yaml', 91 109 output: 'validators-types', 92 110 plugins: [ 93 111 {
+21 -2
packages/openapi-ts/src/plugins/valibot/types.ts
··· 2 2 Casing, 3 3 DefinePlugin, 4 4 FeatureToggle, 5 + IR, 5 6 NameTransformer, 6 7 NamingOptions, 7 8 Plugin, 8 9 } from '@hey-api/shared'; 9 10 11 + import type { $, DollarTsDsl } from '../../ts-dsl'; 10 12 import type { IApi } from './api'; 11 13 import type { Resolvers } from './resolvers'; 12 14 ··· 62 64 * with some additional metadata for documentation, code generation, AI 63 65 * structured outputs, form validation, and other purposes. 64 66 * 67 + * Can be: 68 + * - `boolean`: Shorthand for the default metadata builder. When `true`, 69 + * attaches `{ description }` from the schema (if present) to the 70 + * generated Valibot schema via the metadata action. 71 + * - `function`: Custom metadata builder. Receives `{ $, node, schema }`, 72 + * where `node` is a pre-initialized `$.object()` node. Add properties to 73 + * `node` to populate the metadata object. Return value is ignored; an 74 + * empty `node` skips metadata for that schema. 75 + * 65 76 * @default false 66 77 */ 67 - metadata?: boolean; 78 + metadata?: 79 + | boolean 80 + | (( 81 + ctx: DollarTsDsl & { node: ReturnType<typeof $.object>; schema: IR.SchemaObject }, 82 + ) => void); 68 83 /** 69 84 * Configuration for request-specific Valibot schemas. 70 85 * ··· 184 199 /** Configuration for reusable schema definitions. */ 185 200 definitions: NamingOptions & FeatureToggle; 186 201 /** Enable Valibot metadata support? */ 187 - metadata: boolean; 202 + metadata: 203 + | boolean 204 + | (( 205 + ctx: DollarTsDsl & { node: ReturnType<typeof $.object>; schema: IR.SchemaObject }, 206 + ) => void); 188 207 /** Configuration for request-specific Valibot schemas. */ 189 208 requests: NamingOptions & FeatureToggle; 190 209 /** Configuration for response-specific Valibot schemas. */
+18 -11
packages/openapi-ts/src/plugins/valibot/v1/walker.ts
··· 202 202 }; 203 203 }, 204 204 postProcess(result, schema, ctx) { 205 - if (ctx.plugin.config.metadata && schema.description) { 206 - const v = ctx.plugin.external('valibot.v'); 207 - const metadataExpr = $(v) 208 - .attr(identifiers.actions.metadata) 209 - .call($.object().prop('description', $.literal(schema.description))); 210 - 211 - return { 212 - meta: result.meta, 213 - pipes: [...result.pipes, metadataExpr], 214 - }; 205 + const metadata = ctx.plugin.config.metadata; 206 + if (!metadata) { 207 + return result; 208 + } 209 + const node = $.object(); 210 + if (typeof metadata === 'function') { 211 + metadata({ $, node, schema }); 212 + } else if (schema.description) { 213 + node.prop('description', $.literal(schema.description)); 214 + } 215 + if (node.isEmpty) { 216 + return result; 215 217 } 218 + const v = ctx.plugin.external('valibot.v'); 219 + const metadataExpr = $(v).attr(identifiers.actions.metadata).call(node); 216 220 217 - return result; 221 + return { 222 + meta: result.meta, 223 + pipes: [...result.pipes, metadataExpr], 224 + }; 218 225 }, 219 226 reference($ref, schema, ctx) { 220 227 const v = ctx.plugin.external('valibot.v');
+21 -14
packages/openapi-ts/src/plugins/zod/mini/walker.ts
··· 204 204 }; 205 205 }, 206 206 postProcess(result, schema, ctx) { 207 - if (ctx.plugin.config.metadata && schema.description) { 208 - const z = ctx.plugin.external('zod.z'); 209 - return { 210 - ...result, 211 - expression: { 212 - expression: result.expression.expression 213 - .attr(identifiers.register) 214 - .call( 215 - $(z).attr(identifiers.globalRegistry), 216 - $.object().pretty().prop('description', $.literal(schema.description)), 217 - ), 218 - }, 219 - }; 207 + const metadata = ctx.plugin.config.metadata; 208 + if (!metadata) { 209 + return result; 210 + } 211 + const node = $.object(); 212 + if (typeof metadata === 'function') { 213 + metadata({ $, node, schema }); 214 + } else if (schema.description) { 215 + node.pretty().prop('description', $.literal(schema.description)); 216 + } 217 + if (node.isEmpty) { 218 + return result; 220 219 } 221 - return result; 220 + const z = ctx.plugin.external('zod.z'); 221 + return { 222 + ...result, 223 + expression: { 224 + expression: result.expression.expression 225 + .attr(identifiers.register) 226 + .call($(z).attr(identifiers.globalRegistry), node), 227 + }, 228 + }; 222 229 }, 223 230 reference($ref, schema, ctx) { 224 231 const z = ctx.plugin.external('zod.z');
+22 -2
packages/openapi-ts/src/plugins/zod/types.ts
··· 2 2 Casing, 3 3 DefinePlugin, 4 4 FeatureToggle, 5 + IR, 5 6 NameTransformer, 6 7 NamingOptions, 7 8 Plugin, 8 9 } from '@hey-api/shared'; 9 10 11 + import type { $, DollarTsDsl } from '../../ts-dsl'; 10 12 import type { IApi } from './api'; 11 13 import type { Resolvers } from './resolvers'; 12 14 import type { TypeOptions } from './shared/types'; ··· 140 142 * some additional metadata for documentation, code generation, AI 141 143 * structured outputs, form validation, and other purposes. 142 144 * 145 + * Can be: 146 + * - `boolean`: Shorthand for the default metadata builder. When `true`, 147 + * attaches `{ description }` from the schema (if present) to the 148 + * generated Zod schema via the metadata API. 149 + * - `function`: Custom metadata builder. Receives `{ $, node, schema }`, 150 + * where `node` is a pre-initialized `$.object().pretty()` node. Add 151 + * properties to `node` to populate the metadata object. Return value is 152 + * ignored; an empty `node` skips metadata for that schema. 153 + * Note: **not supported for Zod 3** (use `boolean` only). 154 + * 143 155 * @default false 144 156 */ 145 - metadata?: boolean; 157 + metadata?: 158 + | boolean 159 + | (( 160 + ctx: DollarTsDsl & { node: ReturnType<typeof $.object>; schema: IR.SchemaObject }, 161 + ) => void); 146 162 /** 147 163 * Configuration for request-specific Zod schemas. 148 164 * ··· 425 441 /** Configuration for reusable schema definitions. */ 426 442 definitions: NamingOptions & FeatureToggle & TypeOptions; 427 443 /** Enable Zod metadata support? */ 428 - metadata: boolean; 444 + metadata: 445 + | boolean 446 + | (( 447 + ctx: DollarTsDsl & { node: ReturnType<typeof $.object>; schema: IR.SchemaObject }, 448 + ) => void); 429 449 /** Configuration for request-specific Zod schemas. */ 430 450 requests: NamingOptions & FeatureToggle & TypeOptions; 431 451 /** Configuration for response-specific Zod schemas. */
+21 -14
packages/openapi-ts/src/plugins/zod/v4/walker.ts
··· 221 221 }; 222 222 }, 223 223 postProcess(result, schema, ctx) { 224 - if (ctx.plugin.config.metadata && schema.description) { 225 - const z = ctx.plugin.external('zod.z'); 226 - return { 227 - ...result, 228 - expression: { 229 - expression: result.expression.expression 230 - .attr(identifiers.register) 231 - .call( 232 - $(z).attr(identifiers.globalRegistry), 233 - $.object().pretty().prop('description', $.literal(schema.description)), 234 - ), 235 - }, 236 - }; 224 + const metadata = ctx.plugin.config.metadata; 225 + if (!metadata) { 226 + return result; 227 + } 228 + const node = $.object(); 229 + if (typeof metadata === 'function') { 230 + metadata({ $, node, schema }); 231 + } else if (schema.description) { 232 + node.pretty().prop('description', $.literal(schema.description)); 233 + } 234 + if (node.isEmpty) { 235 + return result; 237 236 } 238 - return result; 237 + const z = ctx.plugin.external('zod.z'); 238 + return { 239 + ...result, 240 + expression: { 241 + expression: result.expression.expression 242 + .attr(identifiers.register) 243 + .call($(z).attr(identifiers.globalRegistry), node), 244 + }, 245 + }; 239 246 }, 240 247 reference($ref, schema, ctx) { 241 248 const z = ctx.plugin.external('zod.z');