From ccbb1cd75b29bf12b625f66ebbc16d5a7d251905 Mon Sep 17 00:00:00 2001 From: Julien Winant Date: Fri, 28 Mar 2025 17:34:36 +0100 Subject: [PATCH 1/2] Add options for datetime into zod plugin --- packages/openapi-ts/src/plugins/zod/plugin.ts | 34 +++++++++++++++++++ .../openapi-ts/src/plugins/zod/types.d.ts | 13 +++++-- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/packages/openapi-ts/src/plugins/zod/plugin.ts b/packages/openapi-ts/src/plugins/zod/plugin.ts index 960dca9ce..0167f12e1 100644 --- a/packages/openapi-ts/src/plugins/zod/plugin.ts +++ b/packages/openapi-ts/src/plugins/zod/plugin.ts @@ -40,10 +40,12 @@ const zIdentifier = compiler.identifier({ text: 'z' }); const nameTransformer = (name: string) => `z-${name}`; const arrayTypeToZodSchema = ({ + config, context, result, schema, }: { + config: Omit; context: IR.Context; result: Result; schema: SchemaWithType<'array'>; @@ -73,6 +75,7 @@ const arrayTypeToZodSchema = ({ // at least one item is guaranteed const itemExpressions = schema.items!.map((item) => schemaToZodSchema({ + config, context, result, schema: item, @@ -331,10 +334,12 @@ const numberTypeToZodSchema = ({ }; const objectTypeToZodSchema = ({ + config, context, result, schema, }: { + config: Omit; context: IR.Context; result: Result; schema: SchemaWithType<'object'>; @@ -353,6 +358,7 @@ const objectTypeToZodSchema = ({ const isRequired = required.includes(name); const propertyExpression = schemaToZodSchema({ + config, context, optional: !isRequired, result, @@ -432,8 +438,10 @@ const objectTypeToZodSchema = ({ }; const stringTypeToZodSchema = ({ + config, schema, }: { + config: Omit; context: IR.Context; schema: SchemaWithType<'string'>; }) => { @@ -463,6 +471,11 @@ const stringTypeToZodSchema = ({ expression: stringExpression, name: compiler.identifier({ text: 'datetime' }), }), + parameters: [ + config.dateTimeOptions + ? JSON.stringify(config.dateTimeOptions) + : undefined, + ], }); break; case 'ipv4': @@ -642,10 +655,12 @@ const voidTypeToZodSchema = ({ }; const schemaTypeToZodSchema = ({ + config, context, result, schema, }: { + config: Omit; context: IR.Context; result: Result; schema: IR.SchemaObject; @@ -653,6 +668,7 @@ const schemaTypeToZodSchema = ({ switch (schema.type as Required['type']) { case 'array': return arrayTypeToZodSchema({ + config, context, result, schema: schema as SchemaWithType<'array'>, @@ -685,12 +701,14 @@ const schemaTypeToZodSchema = ({ }); case 'object': return objectTypeToZodSchema({ + config, context, result, schema: schema as SchemaWithType<'object'>, }); case 'string': return stringTypeToZodSchema({ + config, context, schema: schema as SchemaWithType<'string'>, }); @@ -718,10 +736,12 @@ const schemaTypeToZodSchema = ({ }; const operationToZodSchema = ({ + config, context, operation, result, }: { + config: Omit; context: IR.Context; operation: IR.OperationObject; result: Result; @@ -736,6 +756,7 @@ const operationToZodSchema = ({ id: operation.id, type: 'response', }), + config, context, result, schema: response, @@ -746,6 +767,7 @@ const operationToZodSchema = ({ const schemaToZodSchema = ({ $ref, + config, context, optional, result, @@ -755,6 +777,7 @@ const schemaToZodSchema = ({ * When $ref is supplied, a node will be emitted to the file. */ $ref?: string; + config: Omit; context: IR.Context; /** * Accept `optional` to handle optional object properties. We can't handle @@ -798,6 +821,7 @@ const schemaToZodSchema = ({ if (!identifierRef.name) { const ref = context.resolveIrRef(schema.$ref); expression = schemaToZodSchema({ + config, context, result, schema: ref, @@ -836,6 +860,7 @@ const schemaToZodSchema = ({ } } else if (schema.type) { expression = schemaTypeToZodSchema({ + config, context, result, schema, @@ -846,6 +871,7 @@ const schemaToZodSchema = ({ if (schema.items) { const itemTypes = schema.items.map((item) => schemaToZodSchema({ + config, context, result, schema: item, @@ -895,6 +921,7 @@ const schemaToZodSchema = ({ } } else { expression = schemaToZodSchema({ + config, context, result, schema, @@ -903,6 +930,7 @@ const schemaToZodSchema = ({ } else { // catch-all fallback for failed schemas expression = schemaTypeToZodSchema({ + config, context, result, schema: { @@ -982,6 +1010,10 @@ export const handler: Plugin.Handler = ({ context, plugin }) => { name: 'z', }); + const config: Omit = { + dateTimeOptions: plugin.dateTimeOptions, + }; + context.subscribe('operation', ({ operation }) => { const result: Result = { circularReferenceTracker: new Set(), @@ -989,6 +1021,7 @@ export const handler: Plugin.Handler = ({ context, plugin }) => { }; operationToZodSchema({ + config, context, operation, result, @@ -1003,6 +1036,7 @@ export const handler: Plugin.Handler = ({ context, plugin }) => { schemaToZodSchema({ $ref, + config, context, result, schema, diff --git a/packages/openapi-ts/src/plugins/zod/types.d.ts b/packages/openapi-ts/src/plugins/zod/types.d.ts index 5a167cf2d..13eb5c3a2 100644 --- a/packages/openapi-ts/src/plugins/zod/types.d.ts +++ b/packages/openapi-ts/src/plugins/zod/types.d.ts @@ -2,6 +2,15 @@ import type { Plugin } from '../types'; export interface Config extends Plugin.Name<'zod'> { + /** + * Zod options for the generated schema about datetimes. + * https://zod.dev/?id=datetimes + */ + dateTimeOptions?: { + local?: boolean; + offset?: boolean; + precision?: number; + }; /** * Should the exports from the generated files be re-exported in the index * barrel file? @@ -13,8 +22,8 @@ export interface Config extends Plugin.Name<'zod'> { * Customise the Zod schema name. By default, `z{{name}}` is used, * where `name` is a definition name or an operation name. */ - // nameBuilder?: (model: IR.OperationObject | IR.SchemaObject) => string; - /** +// nameBuilder?: (model: IR.OperationObject | IR.SchemaObject) => string; +/** * Name of the generated file. * * @default 'zod' From 1b89885568e276eeb6524d8ef1feea31c49df498 Mon Sep 17 00:00:00 2001 From: Julien Winant Date: Sat, 29 Mar 2025 08:27:03 +0100 Subject: [PATCH 2/2] Use plugin as variable instead of a new config variable --- packages/openapi-ts/src/plugins/zod/plugin.ts | 58 +++++++++---------- .../openapi-ts/src/plugins/zod/types.d.ts | 4 +- 2 files changed, 29 insertions(+), 33 deletions(-) diff --git a/packages/openapi-ts/src/plugins/zod/plugin.ts b/packages/openapi-ts/src/plugins/zod/plugin.ts index 0167f12e1..94854f6e5 100644 --- a/packages/openapi-ts/src/plugins/zod/plugin.ts +++ b/packages/openapi-ts/src/plugins/zod/plugin.ts @@ -40,13 +40,13 @@ const zIdentifier = compiler.identifier({ text: 'z' }); const nameTransformer = (name: string) => `z-${name}`; const arrayTypeToZodSchema = ({ - config, context, + plugin, result, schema, }: { - config: Omit; context: IR.Context; + plugin: Config; result: Result; schema: SchemaWithType<'array'>; }): ts.CallExpression => { @@ -75,8 +75,8 @@ const arrayTypeToZodSchema = ({ // at least one item is guaranteed const itemExpressions = schema.items!.map((item) => schemaToZodSchema({ - config, context, + plugin, result, schema: item, }), @@ -334,13 +334,13 @@ const numberTypeToZodSchema = ({ }; const objectTypeToZodSchema = ({ - config, context, + plugin, result, schema, }: { - config: Omit; context: IR.Context; + plugin: Config; result: Result; schema: SchemaWithType<'object'>; }) => { @@ -358,9 +358,9 @@ const objectTypeToZodSchema = ({ const isRequired = required.includes(name); const propertyExpression = schemaToZodSchema({ - config, context, optional: !isRequired, + plugin, result, schema: property, }); @@ -438,11 +438,11 @@ const objectTypeToZodSchema = ({ }; const stringTypeToZodSchema = ({ - config, + plugin, schema, }: { - config: Omit; context: IR.Context; + plugin: Config; schema: SchemaWithType<'string'>; }) => { if (typeof schema.const === 'string') { @@ -472,8 +472,8 @@ const stringTypeToZodSchema = ({ name: compiler.identifier({ text: 'datetime' }), }), parameters: [ - config.dateTimeOptions - ? JSON.stringify(config.dateTimeOptions) + plugin.dateTimeOptions + ? JSON.stringify(plugin.dateTimeOptions) : undefined, ], }); @@ -655,21 +655,21 @@ const voidTypeToZodSchema = ({ }; const schemaTypeToZodSchema = ({ - config, context, + plugin, result, schema, }: { - config: Omit; context: IR.Context; + plugin: Config; result: Result; schema: IR.SchemaObject; }): ts.Expression => { switch (schema.type as Required['type']) { case 'array': return arrayTypeToZodSchema({ - config, context, + plugin, result, schema: schema as SchemaWithType<'array'>, }); @@ -701,15 +701,15 @@ const schemaTypeToZodSchema = ({ }); case 'object': return objectTypeToZodSchema({ - config, context, + plugin, result, schema: schema as SchemaWithType<'object'>, }); case 'string': return stringTypeToZodSchema({ - config, context, + plugin, schema: schema as SchemaWithType<'string'>, }); case 'tuple': @@ -736,14 +736,14 @@ const schemaTypeToZodSchema = ({ }; const operationToZodSchema = ({ - config, context, operation, + plugin, result, }: { - config: Omit; context: IR.Context; operation: IR.OperationObject; + plugin: Config; result: Result; }) => { if (operation.responses) { @@ -756,8 +756,8 @@ const operationToZodSchema = ({ id: operation.id, type: 'response', }), - config, context, + plugin, result, schema: response, }); @@ -767,9 +767,9 @@ const operationToZodSchema = ({ const schemaToZodSchema = ({ $ref, - config, context, optional, + plugin, result, schema, }: { @@ -777,7 +777,6 @@ const schemaToZodSchema = ({ * When $ref is supplied, a node will be emitted to the file. */ $ref?: string; - config: Omit; context: IR.Context; /** * Accept `optional` to handle optional object properties. We can't handle @@ -785,6 +784,7 @@ const schemaToZodSchema = ({ * `.default()` which is handled in this function. */ optional?: boolean; + plugin: Config; result: Result; schema: IR.SchemaObject; }): ts.Expression => { @@ -821,8 +821,8 @@ const schemaToZodSchema = ({ if (!identifierRef.name) { const ref = context.resolveIrRef(schema.$ref); expression = schemaToZodSchema({ - config, context, + plugin, result, schema: ref, }); @@ -860,8 +860,8 @@ const schemaToZodSchema = ({ } } else if (schema.type) { expression = schemaTypeToZodSchema({ - config, context, + plugin, result, schema, }); @@ -871,8 +871,8 @@ const schemaToZodSchema = ({ if (schema.items) { const itemTypes = schema.items.map((item) => schemaToZodSchema({ - config, context, + plugin, result, schema: item, }), @@ -921,8 +921,8 @@ const schemaToZodSchema = ({ } } else { expression = schemaToZodSchema({ - config, context, + plugin, result, schema, }); @@ -930,8 +930,8 @@ const schemaToZodSchema = ({ } else { // catch-all fallback for failed schemas expression = schemaTypeToZodSchema({ - config, context, + plugin, result, schema: { type: 'unknown', @@ -1010,10 +1010,6 @@ export const handler: Plugin.Handler = ({ context, plugin }) => { name: 'z', }); - const config: Omit = { - dateTimeOptions: plugin.dateTimeOptions, - }; - context.subscribe('operation', ({ operation }) => { const result: Result = { circularReferenceTracker: new Set(), @@ -1021,9 +1017,9 @@ export const handler: Plugin.Handler = ({ context, plugin }) => { }; operationToZodSchema({ - config, context, operation, + plugin, result, }); }); @@ -1036,8 +1032,8 @@ export const handler: Plugin.Handler = ({ context, plugin }) => { schemaToZodSchema({ $ref, - config, context, + plugin, result, schema, }); diff --git a/packages/openapi-ts/src/plugins/zod/types.d.ts b/packages/openapi-ts/src/plugins/zod/types.d.ts index 13eb5c3a2..d3f9ca206 100644 --- a/packages/openapi-ts/src/plugins/zod/types.d.ts +++ b/packages/openapi-ts/src/plugins/zod/types.d.ts @@ -22,8 +22,8 @@ export interface Config extends Plugin.Name<'zod'> { * Customise the Zod schema name. By default, `z{{name}}` is used, * where `name` is a definition name or an operation name. */ -// nameBuilder?: (model: IR.OperationObject | IR.SchemaObject) => string; -/** + // nameBuilder?: (model: IR.OperationObject | IR.SchemaObject) => string; + /** * Name of the generated file. * * @default 'zod'