@kubb/plugin-zod 5.0.0-beta.4 → 5.0.0-beta.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,57 +1,71 @@
1
- import { camelCase, pascalCase } from '@internals/utils'
1
+ import { camelCase, ensureValidVarName, pascalCase, toFilePath } from '@internals/utils'
2
2
  import { defineResolver } from '@kubb/core'
3
3
  import type { PluginZod } from '../types.ts'
4
4
 
5
5
  /**
6
- * Naming convention resolver for Zod plugin.
6
+ * Default resolver used by `@kubb/plugin-zod`. Decides the names and file
7
+ * paths for every generated Zod schema. Schemas use camelCase with a
8
+ * `Schema` suffix (`listPetsSchema`); their inferred types use PascalCase
9
+ * with a `SchemaType` suffix (`PetSchemaType`), so the value and the type
10
+ * never share an identifier even when the schema name is all-uppercase.
7
11
  *
8
- * Provides default naming helpers using camelCase with a `Schema` suffix for schemas.
12
+ * @example Resolve schema and type names
13
+ * ```ts
14
+ * import { resolverZod } from '@kubb/plugin-zod'
9
15
  *
10
- * @example
11
- * `resolverZod.default('list pets', 'function') // 'listPetsSchema'`
16
+ * resolverZod.default('list pets', 'function') // 'listPetsSchema'
17
+ * resolverZod.resolveSchemaTypeName('pet') // 'PetSchemaType'
18
+ * ```
12
19
  */
13
- export const resolverZod = defineResolver<PluginZod>((ctx) => {
20
+ export const resolverZod = defineResolver<PluginZod>(() => {
14
21
  return {
15
22
  name: 'default',
16
23
  pluginName: 'plugin-zod',
17
24
  default(name, type) {
18
- return camelCase(name, { isFile: type === 'file', suffix: type ? 'schema' : undefined })
25
+ if (type === 'file') return toFilePath(name, (part) => camelCase(part, { suffix: 'schema' }))
26
+ return ensureValidVarName(camelCase(name, { suffix: type ? 'schema' : undefined }))
19
27
  },
20
28
  resolveSchemaName(name) {
21
- return camelCase(name, { suffix: 'schema' })
29
+ return ensureValidVarName(camelCase(name, { suffix: 'schema' }))
22
30
  },
23
31
  resolveSchemaTypeName(name) {
24
- return pascalCase(name, { suffix: 'schema' })
32
+ return ensureValidVarName(pascalCase(name, { suffix: 'schema type' }))
33
+ },
34
+ resolveInputSchemaName(name) {
35
+ return this.resolveSchemaName(`${name} input`)
36
+ },
37
+ resolveInputSchemaTypeName(name) {
38
+ return this.resolveSchemaTypeName(`${name} input`)
25
39
  },
26
40
  resolveTypeName(name) {
27
- return pascalCase(name)
41
+ return ensureValidVarName(pascalCase(name, { suffix: 'type' }))
28
42
  },
29
43
  resolvePathName(name, type) {
30
- return ctx.default(name, type)
44
+ return this.default(name, type)
31
45
  },
32
46
  resolveParamName(node, param) {
33
- return ctx.resolveSchemaName(`${node.operationId} ${param.in} ${param.name}`)
47
+ return this.resolveSchemaName(`${node.operationId} ${param.in} ${param.name}`)
34
48
  },
35
49
  resolveResponseStatusName(node, statusCode) {
36
- return ctx.resolveSchemaName(`${node.operationId} Status ${statusCode}`)
50
+ return this.resolveSchemaName(`${node.operationId} Status ${statusCode}`)
37
51
  },
38
52
  resolveDataName(node) {
39
- return ctx.resolveSchemaName(`${node.operationId} Data`)
53
+ return this.resolveSchemaName(`${node.operationId} Data`)
40
54
  },
41
55
  resolveResponsesName(node) {
42
- return ctx.resolveSchemaName(`${node.operationId} Responses`)
56
+ return this.resolveSchemaName(`${node.operationId} Responses`)
43
57
  },
44
58
  resolveResponseName(node) {
45
- return ctx.resolveSchemaName(`${node.operationId} Response`)
59
+ return this.resolveSchemaName(`${node.operationId} Response`)
46
60
  },
47
61
  resolvePathParamsName(node, param) {
48
- return ctx.resolveParamName(node, param)
62
+ return this.resolveParamName(node, param)
49
63
  },
50
64
  resolveQueryParamsName(node, param) {
51
- return ctx.resolveParamName(node, param)
65
+ return this.resolveParamName(node, param)
52
66
  },
53
67
  resolveHeaderParamsName(node, param) {
54
- return ctx.resolveParamName(node, param)
68
+ return this.resolveParamName(node, param)
55
69
  },
56
70
  }
57
71
  })
package/src/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ast, Exclude, Generator, Group, Include, Output, Override, PluginFactoryOptions, Resolver } from '@kubb/core'
1
+ import type { ast, Exclude, Generator, Group, Include, Output, OutputOptions, Override, PluginFactoryOptions, Resolver } from '@kubb/core'
2
2
  import type { PrinterZodNodes } from './printers/printerZod.ts'
3
3
  import type { PrinterZodMiniNodes } from './printers/printerZodMini.ts'
4
4
 
@@ -15,14 +15,29 @@ export type ResolverZod = Resolver &
15
15
  * Resolves the schema type name (inferred type from schema).
16
16
  *
17
17
  * @example Schema type names
18
- * `resolver.resolveSchemaTypeName('pet') // → 'Pet'`
18
+ * `resolver.resolveSchemaTypeName('pet') // → 'PetSchemaType'`
19
19
  */
20
20
  resolveSchemaTypeName(this: ResolverZod, name: string): string
21
+ /**
22
+ * Resolves the schema function name for the request (input) direction of a
23
+ * date-bearing component, where `Date` is encoded back to a wire `string`.
24
+ *
25
+ * @example Input schema names
26
+ * `resolver.resolveInputSchemaName('order') // → 'orderInputSchema'`
27
+ */
28
+ resolveInputSchemaName(this: ResolverZod, name: string): string
29
+ /**
30
+ * Resolves the inferred type name for the request (input) direction variant.
31
+ *
32
+ * @example Input schema type names
33
+ * `resolver.resolveInputSchemaTypeName('order') // → 'OrderInputSchemaType'`
34
+ */
35
+ resolveInputSchemaTypeName(this: ResolverZod, name: string): string
21
36
  /**
22
37
  * Resolves the generated type name from the schema.
23
38
  *
24
39
  * @example Type names
25
- * `resolver.resolveTypeName('pet') // → 'Pet'`
40
+ * `resolver.resolveTypeName('petSchema') // → 'PetSchemaType'`
26
41
  */
27
42
  resolveTypeName(this: ResolverZod, name: string): string
28
43
  /**
@@ -73,87 +88,104 @@ export type ResolverZod = Resolver &
73
88
  resolveHeaderParamsName(this: ResolverZod, node: ast.OperationNode, param: ast.ParameterNode): string
74
89
  }
75
90
 
76
- export type Options = {
77
- /**
78
- * @default 'zod'
79
- */
80
- output?: Output
81
- /**
82
- * Group the Zod schemas based on the provided name.
83
- */
84
- group?: Group
91
+ /**
92
+ * Where the generated Zod schemas are written and how they are exported, plus the optional
93
+ * `group` strategy. The `group` option organizes `output.mode: 'directory'` output into per-tag or per-path subdirectories.
94
+ *
95
+ * @default { path: 'zod', barrel: { type: 'named' } }
96
+ */
97
+ export type Options = OutputOptions & {
85
98
  /**
86
- * Tags, operations, or paths to exclude from generation.
99
+ * Skip operations matching at least one entry in the list.
87
100
  */
88
101
  exclude?: Array<Exclude>
89
102
  /**
90
- * Tags, operations, or paths to include in generation.
103
+ * Restrict generation to operations matching at least one entry in the list.
91
104
  */
92
105
  include?: Array<Include>
93
106
  /**
94
- * Override options for specific tags, operations, or paths.
107
+ * Apply a different options object to operations matching a pattern.
95
108
  */
96
109
  override?: Array<Override<ResolvedOptions>>
97
110
  /**
98
- * Import path for Zod package.
111
+ * Module specifier used in the `import { z } from '...'` statement.
112
+ * Use `'zod/mini'` for the tree-shakeable bundle.
99
113
  *
100
- * @default 'zod'
114
+ * @default mini ? 'zod/mini' : 'zod'
101
115
  */
102
116
  importPath?: 'zod' | 'zod/mini' | (string & {})
103
117
  /**
104
- * Add TypeScript type annotations to generated schemas.
118
+ * Tie each Zod schema to its TypeScript type from `@kubb/plugin-ts`. Requires
119
+ * `@kubb/plugin-ts` in the plugins list. TypeScript fails compilation when the
120
+ * schema drifts from the type.
105
121
  */
106
122
  typed?: boolean
107
123
  /**
108
- * Return schemas as inferred types using `z.infer`.
124
+ * Export a `z.infer<typeof schema>` type alias next to every generated schema.
125
+ * Lets the Zod schema act as the single source of truth.
109
126
  */
110
127
  inferred?: boolean
111
128
  /**
112
- * Apply coercion to string values or configure coercion per type.
129
+ * Wrap schemas in `z.coerce` so input is coerced before validation. Useful for
130
+ * form data and query params where everything arrives as a string.
131
+ * - `true` coerces strings, numbers, and dates.
132
+ * - Object form picks per-primitive coercion.
133
+ *
134
+ * @default false
135
+ * @see https://zod.dev/?id=coercion-for-primitives
113
136
  */
114
137
  coercion?: boolean | { dates?: boolean; strings?: boolean; numbers?: boolean }
115
138
  /**
116
- * Generate operation-level schemas (grouped by operationId).
139
+ * Emit an `operations.ts` file with request body, query/path params, and per-status
140
+ * response schemas grouped by operation.
117
141
  */
118
142
  operations?: boolean
119
143
  /**
120
- * Validator to use for UUID format: `uuid` or `guid`.
144
+ * Validator for `format: uuid` properties.
145
+ * - `'uuid'` — `z.uuid()`. Standard RFC 4122.
146
+ * - `'guid'` — `z.guid()`. Accepts Microsoft-style GUIDs.
121
147
  *
122
148
  * @default 'uuid'
123
149
  */
124
150
  guidType?: 'uuid' | 'guid'
125
151
  /**
126
- * Use Zod Mini's functional API for better tree-shaking.
152
+ * Switch to Zod Mini's functional API for better tree-shaking. Also defaults
153
+ * `importPath` to `'zod/mini'`.
127
154
  *
128
155
  * @default false
156
+ * @beta
129
157
  */
130
158
  mini?: boolean
131
159
  /**
132
- * Callback to wrap the generated schema output.
133
- *
134
- * Useful for adding metadata like `.openapi()` or extension helpers.
160
+ * Wrap the generated Zod schema string with extra calls. Receives the raw output
161
+ * and the originating `SchemaNode`. Useful for round-tripping OpenAPI metadata
162
+ * back into Zod (e.g. `.openapi(...)`).
135
163
  */
136
164
  wrapOutput?: (arg: { output: string; schema: ast.SchemaNode }) => string | undefined
137
165
  /**
138
- * Apply casing to parameter names.
166
+ * Rename properties inside path/query/header schemas. Body schemas are unaffected.
167
+ *
168
+ * @note Must match the value of `paramsCasing` on `@kubb/plugin-ts`.
139
169
  */
140
170
  paramsCasing?: 'camelcase'
141
171
  /**
142
- * Additional generators alongside the default generators.
172
+ * Custom generators that run alongside the built-in Zod generators.
143
173
  */
144
174
  generators?: Array<Generator<PluginZod>>
145
175
  /**
146
- * Override naming conventions for schema names and types.
176
+ * Override how schema and operation names are built. Methods you omit fall back
177
+ * to the default `resolverZod`.
147
178
  */
148
179
  resolver?: Partial<ResolverZod> & ThisType<ResolverZod>
149
180
  /**
150
- * Override printer node handlers to customize rendering of specific schema types.
181
+ * Replace the Zod handler for a specific schema type (`'integer'`, `'date'`, ...).
182
+ * When `mini: true`, overrides target the Zod Mini printer instead.
151
183
  */
152
184
  printer?: {
153
185
  nodes?: PrinterZodNodes | PrinterZodMiniNodes
154
186
  }
155
187
  /**
156
- * AST visitor to transform schema and operation nodes.
188
+ * AST visitor applied to each schema or operation node before printing.
157
189
  */
158
190
  transformer?: ast.Visitor
159
191
  }
@@ -163,7 +195,7 @@ type ResolvedOptions = {
163
195
  exclude: Array<Exclude>
164
196
  include: Array<Include> | undefined
165
197
  override: Array<Override<ResolvedOptions>>
166
- group: Group | undefined
198
+ group: Group | null
167
199
  typed: NonNullable<Options['typed']>
168
200
  inferred: NonNullable<Options['inferred']>
169
201
  importPath: NonNullable<Options['importPath']>
package/src/utils.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { stringify, toRegExpString } from '@internals/utils'
1
+ import { extractRefName, stringify, toRegExpString } from '@kubb/ast/utils'
2
2
  import { ast } from '@kubb/core'
3
3
  import type { PluginZod, ResolverZod } from './types.ts'
4
4
 
@@ -12,6 +12,99 @@ export function shouldCoerce(coercion: PluginZod['resolvedOptions']['coercion']
12
12
  return !!coercion[type]
13
13
  }
14
14
 
15
+ /**
16
+ * A codec for a schema node whose runtime type differs from its JSON wire type:
17
+ * the output (response) schema decodes wire → runtime, and the input (request)
18
+ * variant encodes runtime → wire.
19
+ *
20
+ * To support another codec type, append a `Codec` to `codecs` and route that
21
+ * type's printer node handler through `getCodec`.
22
+ */
23
+ export type Codec = {
24
+ /**
25
+ * Whether this node is encoded/decoded by this codec.
26
+ */
27
+ matches(node: ast.SchemaNode): boolean
28
+ /**
29
+ * Output direction (response): decode the wire value into the runtime type.
30
+ */
31
+ decode(node: ast.SchemaNode): string
32
+ /**
33
+ * Input direction (request): encode the runtime value back to the wire value.
34
+ */
35
+ encode(node: ast.SchemaNode): string
36
+ }
37
+
38
+ /**
39
+ * `dateType: 'date'` fields are typed as `Date` but travel as ISO `string`s.
40
+ * Output decodes `string → Date`; input encodes `Date → string`, preserving the
41
+ * `date` (`YYYY-MM-DD`) vs `date-time` precision carried on `node.format`.
42
+ */
43
+ const dateCodec: Codec = {
44
+ matches(node) {
45
+ return node.type === 'date' && node.representation === 'date'
46
+ },
47
+ decode(node) {
48
+ return node.format === 'date' ? 'z.iso.date().transform((value) => new Date(value))' : 'z.iso.datetime().transform((value) => new Date(value))'
49
+ },
50
+ encode(node) {
51
+ return node.format === 'date' ? 'z.date().transform((value) => value.toISOString().slice(0, 10))' : 'z.date().transform((value) => value.toISOString())'
52
+ },
53
+ }
54
+
55
+ /**
56
+ * Registered codecs, checked in order.
57
+ */
58
+ const codecs: Array<Codec> = [dateCodec]
59
+
60
+ /**
61
+ * Returns the codec for this node, or `undefined` when the node needs no
62
+ * encode/decode (its wire and runtime types match).
63
+ */
64
+ export function getCodec(node: ast.SchemaNode | undefined): Codec | undefined {
65
+ if (!node) return undefined
66
+ return codecs.find((codec) => codec.matches(node))
67
+ }
68
+
69
+ /**
70
+ * Returns `true` when the node itself is encoded/decoded by a codec.
71
+ */
72
+ export function hasCodec(node: ast.SchemaNode | undefined): boolean {
73
+ return getCodec(node) !== undefined
74
+ }
75
+
76
+ /**
77
+ * Returns `true` when the schema transitively contains a codec node —
78
+ * a value whose runtime type differs from its wire type (see {@link hasCodec}),
79
+ * so it must be decoded (response) or encoded (request) at the validation boundary.
80
+ * `$ref`s are followed via their resolved schema; a `seen` set guards cycles.
81
+ */
82
+ export function containsCodec(node: ast.SchemaNode | undefined, seen: Set<string> = new Set()): boolean {
83
+ if (!node) return false
84
+
85
+ if (hasCodec(node)) return true
86
+
87
+ if (node.type === 'ref') {
88
+ if (!node.ref) return false
89
+ const refName = extractRefName(node.ref)
90
+ if (refName) {
91
+ if (seen.has(refName)) return false
92
+ seen.add(refName)
93
+ }
94
+ const resolved = ast.syncSchemaRef(node)
95
+ if (resolved.type === 'ref') return false
96
+ return containsCodec(resolved, seen)
97
+ }
98
+
99
+ const children: Array<ast.SchemaNode | undefined> = []
100
+ if ('properties' in node && node.properties) children.push(...node.properties.map((prop) => prop.schema))
101
+ if ('items' in node && node.items) children.push(...node.items)
102
+ if ('members' in node && node.members) children.push(...node.members)
103
+ if ('additionalProperties' in node && node.additionalProperties && node.additionalProperties !== true) children.push(node.additionalProperties)
104
+
105
+ return children.some((child) => containsCodec(child, seen))
106
+ }
107
+
15
108
  /**
16
109
  * Collects all resolved schema names for an operation's parameters and responses
17
110
  * into a single lookup object, useful for building imports and type references.
@@ -39,11 +132,11 @@ export function buildSchemaNames(node: ast.OperationNode, { params, resolver }:
39
132
  responses['default'] = resolver.resolveResponseName(node)
40
133
 
41
134
  return {
42
- request: node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : undefined,
135
+ request: node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : null,
43
136
  parameters: {
44
- path: pathParam ? resolver.resolvePathParamsName(node, pathParam) : undefined,
45
- query: queryParam ? resolver.resolveQueryParamsName(node, queryParam) : undefined,
46
- header: headerParam ? resolver.resolveHeaderParamsName(node, headerParam) : undefined,
137
+ path: pathParam ? resolver.resolvePathParamsName(node, pathParam) : null,
138
+ query: queryParam ? resolver.resolveQueryParamsName(node, queryParam) : null,
139
+ header: headerParam ? resolver.resolveHeaderParamsName(node, headerParam) : null,
47
140
  },
48
141
  responses,
49
142
  errors,
@@ -133,7 +226,7 @@ export function lengthConstraints({ min, max, pattern }: LengthConstraints): str
133
226
  * Build `.check(z.minimum(), z.maximum())` for `zod/mini` numeric constraints.
134
227
  */
135
228
  export function numberChecksMini({ min, max, exclusiveMinimum, exclusiveMaximum, multipleOf }: NumericConstraints): string {
136
- const checks: string[] = []
229
+ const checks: Array<string> = []
137
230
  if (min !== undefined) checks.push(`z.minimum(${min})`)
138
231
  if (max !== undefined) checks.push(`z.maximum(${max})`)
139
232
  if (exclusiveMinimum !== undefined) checks.push(`z.minimum(${exclusiveMinimum}, { exclusive: true })`)
@@ -146,7 +239,7 @@ export function numberChecksMini({ min, max, exclusiveMinimum, exclusiveMaximum,
146
239
  * Build `.check(z.minLength(), z.maxLength(), z.regex())` for `zod/mini` length constraints.
147
240
  */
148
241
  export function lengthChecksMini({ min, max, pattern }: LengthConstraints): string {
149
- const checks: string[] = []
242
+ const checks: Array<string> = []
150
243
  if (min !== undefined) checks.push(`z.minLength(${min})`)
151
244
  if (max !== undefined) checks.push(`z.maxLength(${max})`)
152
245
  if (pattern !== undefined) checks.push(`z.regex(${toRegExpString(pattern, null)})`)
@@ -158,21 +251,14 @@ export function lengthChecksMini({ min, max, pattern }: LengthConstraints): stri
158
251
  * to a schema value string using the chainable Zod v4 API.
159
252
  */
160
253
  export function applyModifiers({ value, nullable, optional, nullish, defaultValue, description }: ModifierOptions): string {
161
- let result = value
162
- if (nullish || (nullable && optional)) {
163
- result = `${result}.nullish()`
164
- } else if (optional) {
165
- result = `${result}.optional()`
166
- } else if (nullable) {
167
- result = `${result}.nullable()`
168
- }
169
- if (defaultValue !== undefined) {
170
- result = `${result}.default(${formatDefault(defaultValue)})`
171
- }
172
- if (description) {
173
- result = `${result}.describe(${stringify(description)})`
174
- }
175
- return result
254
+ const withModifier = (() => {
255
+ if (nullish || (nullable && optional)) return `${value}.nullish()`
256
+ if (optional) return `${value}.optional()`
257
+ if (nullable) return `${value}.nullable()`
258
+ return value
259
+ })()
260
+ const withDefault = defaultValue !== undefined ? `${withModifier}.default(${formatDefault(defaultValue)})` : withModifier
261
+ return description ? `${withDefault}.describe(${stringify(description)})` : withDefault
176
262
  }
177
263
 
178
264
  /**
@@ -180,21 +266,12 @@ export function applyModifiers({ value, nullable, optional, nullish, defaultValu
180
266
  * (`z.nullable()`, `z.optional()`, `z.nullish()`).
181
267
  */
182
268
  export function applyMiniModifiers({ value, nullable, optional, nullish, defaultValue }: Omit<ModifierOptions, 'description'>): string {
183
- let result = value
184
- if (nullish) {
185
- result = `z.nullish(${result})`
186
- } else {
187
- if (nullable) {
188
- result = `z.nullable(${result})`
189
- }
190
- if (optional) {
191
- result = `z.optional(${result})`
192
- }
193
- }
194
- if (defaultValue !== undefined) {
195
- result = `z._default(${result}, ${formatDefault(defaultValue)})`
196
- }
197
- return result
269
+ const withModifier = (() => {
270
+ if (nullish) return `z.nullish(${value})`
271
+ const withNullable = nullable ? `z.nullable(${value})` : value
272
+ return optional ? `z.optional(${withNullable})` : withNullable
273
+ })()
274
+ return defaultValue !== undefined ? `z._default(${withModifier}, ${formatDefault(defaultValue)})` : withModifier
198
275
  }
199
276
 
200
277
  type BuildGroupedParamsSchemaOptions = {