@kubb/plugin-zod 5.0.0-alpha.8 → 5.0.0-beta.3

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.
Files changed (45) hide show
  1. package/LICENSE +17 -10
  2. package/README.md +1 -3
  3. package/dist/index.cjs +1061 -105
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.ts +369 -4
  6. package/dist/index.js +1053 -104
  7. package/dist/index.js.map +1 -1
  8. package/package.json +44 -70
  9. package/src/components/Operations.tsx +25 -18
  10. package/src/components/Zod.tsx +21 -121
  11. package/src/constants.ts +5 -0
  12. package/src/generators/zodGenerator.tsx +174 -160
  13. package/src/index.ts +11 -2
  14. package/src/plugin.ts +67 -156
  15. package/src/printers/printerZod.ts +339 -0
  16. package/src/printers/printerZodMini.ts +295 -0
  17. package/src/resolvers/resolverZod.ts +57 -0
  18. package/src/types.ts +130 -115
  19. package/src/utils.ts +222 -0
  20. package/dist/components-B7zUFnAm.cjs +0 -890
  21. package/dist/components-B7zUFnAm.cjs.map +0 -1
  22. package/dist/components-eECfXVou.js +0 -842
  23. package/dist/components-eECfXVou.js.map +0 -1
  24. package/dist/components.cjs +0 -4
  25. package/dist/components.d.ts +0 -56
  26. package/dist/components.js +0 -2
  27. package/dist/generators-BjPDdJUz.cjs +0 -301
  28. package/dist/generators-BjPDdJUz.cjs.map +0 -1
  29. package/dist/generators-lTWPS6oN.js +0 -290
  30. package/dist/generators-lTWPS6oN.js.map +0 -1
  31. package/dist/generators.cjs +0 -4
  32. package/dist/generators.d.ts +0 -479
  33. package/dist/generators.js +0 -2
  34. package/dist/templates/ToZod.source.cjs +0 -7
  35. package/dist/templates/ToZod.source.cjs.map +0 -1
  36. package/dist/templates/ToZod.source.d.ts +0 -7
  37. package/dist/templates/ToZod.source.js +0 -6
  38. package/dist/templates/ToZod.source.js.map +0 -1
  39. package/dist/types-CoCoOc2u.d.ts +0 -172
  40. package/src/components/index.ts +0 -2
  41. package/src/generators/index.ts +0 -2
  42. package/src/generators/operationsGenerator.tsx +0 -50
  43. package/src/parser.ts +0 -909
  44. package/src/templates/ToZod.source.ts +0 -4
  45. package/templates/ToZod.ts +0 -61
package/src/types.ts CHANGED
@@ -1,172 +1,187 @@
1
- import type { Group, Output, PluginFactoryOptions, ResolveNameParams } from '@kubb/core'
2
- import type { contentType, Oas, SchemaObject } from '@kubb/oas'
3
- import type { Exclude, Include, Override, ResolvePathOptions, Schema } from '@kubb/plugin-oas'
4
- import type { Generator } from '@kubb/plugin-oas/generators'
1
+ import type { ast, Exclude, Generator, Group, Include, Output, Override, PluginFactoryOptions, Resolver } from '@kubb/core'
2
+ import type { PrinterZodNodes } from './printers/printerZod.ts'
3
+ import type { PrinterZodMiniNodes } from './printers/printerZodMini.ts'
4
+
5
+ /**
6
+ * Resolver for Zod that provides naming methods for schema types.
7
+ */
8
+ export type ResolverZod = Resolver &
9
+ ast.OperationParamsResolver & {
10
+ /**
11
+ * Resolves a camelCase schema function name with a `Schema` suffix.
12
+ */
13
+ resolveSchemaName(this: ResolverZod, name: string): string
14
+ /**
15
+ * Resolves the schema type name (inferred type from schema).
16
+ *
17
+ * @example Schema type names
18
+ * `resolver.resolveSchemaTypeName('pet') // → 'Pet'`
19
+ */
20
+ resolveSchemaTypeName(this: ResolverZod, name: string): string
21
+ /**
22
+ * Resolves the generated type name from the schema.
23
+ *
24
+ * @example Type names
25
+ * `resolver.resolveTypeName('pet') // → 'Pet'`
26
+ */
27
+ resolveTypeName(this: ResolverZod, name: string): string
28
+ /**
29
+ * Resolves the output file name for a schema.
30
+ */
31
+ resolvePathName(this: ResolverZod, name: string, type?: 'file' | 'function' | 'type' | 'const'): string
32
+ /**
33
+ * Resolves the name for an operation response by status code.
34
+ *
35
+ * @example Response status names
36
+ * `resolver.resolveResponseStatusName(node, 200) // → 'listPetsStatus200Schema'`
37
+ */
38
+ resolveResponseStatusName(this: ResolverZod, node: ast.OperationNode, statusCode: ast.StatusCode): string
39
+ /**
40
+ * Resolves the name for the collection of all operation responses.
41
+ *
42
+ * @example Responses collection names
43
+ * `resolver.resolveResponsesName(node) // → 'listPetsResponsesSchema'`
44
+ */
45
+ resolveResponsesName(this: ResolverZod, node: ast.OperationNode): string
46
+ /**
47
+ * Resolves the name for the union of all operation responses.
48
+ *
49
+ * @example Response union names
50
+ * `resolver.resolveResponseName(node) // → 'listPetsResponseSchema'`
51
+ */
52
+ resolveResponseName(this: ResolverZod, node: ast.OperationNode): string
53
+ /**
54
+ * Resolves the name for an operation's grouped path parameters type.
55
+ *
56
+ * @example Path parameters names
57
+ * `resolver.resolvePathParamsName(node, param) // → 'deletePetPathPetIdSchema'`
58
+ */
59
+ resolvePathParamsName(this: ResolverZod, node: ast.OperationNode, param: ast.ParameterNode): string
60
+ /**
61
+ * Resolves the name for an operation's grouped query parameters type.
62
+ *
63
+ * @example Query parameters names
64
+ * `resolver.resolveQueryParamsName(node, param) // → 'findPetsByStatusQueryStatusSchema'`
65
+ */
66
+ resolveQueryParamsName(this: ResolverZod, node: ast.OperationNode, param: ast.ParameterNode): string
67
+ /**
68
+ * Resolves the name for an operation's grouped header parameters type.
69
+ *
70
+ * @example Header parameters names
71
+ * `resolver.resolveHeaderParamsName(node, param) // → 'deletePetHeaderApiKeySchema'`
72
+ */
73
+ resolveHeaderParamsName(this: ResolverZod, node: ast.OperationNode, param: ast.ParameterNode): string
74
+ }
5
75
 
6
76
  export type Options = {
7
77
  /**
8
78
  * @default 'zod'
9
79
  */
10
- output?: Output<Oas>
11
- /**
12
- * Define which contentType should be used.
13
- * By default, the first JSON valid mediaType is used
14
- */
15
- contentType?: contentType
80
+ output?: Output
16
81
  /**
17
82
  * Group the Zod schemas based on the provided name.
18
83
  */
19
84
  group?: Group
20
85
  /**
21
- * Array containing exclude parameters to exclude/skip tags/operations/methods/paths.
86
+ * Tags, operations, or paths to exclude from generation.
22
87
  */
23
88
  exclude?: Array<Exclude>
24
89
  /**
25
- * Array containing include parameters to include tags/operations/methods/paths.
90
+ * Tags, operations, or paths to include in generation.
26
91
  */
27
92
  include?: Array<Include>
28
93
  /**
29
- * Array containing override parameters to override `options` based on tags/operations/methods/paths.
94
+ * Override options for specific tags, operations, or paths.
30
95
  */
31
96
  override?: Array<Override<ResolvedOptions>>
32
97
  /**
33
- * Path to Zod
34
- * It used as `import { z } from '${importPath}'`.
35
- * Accepts relative and absolute paths.
36
- * Path is used as-is; relative paths are based on the generated file location.
98
+ * Import path for Zod package.
99
+ *
37
100
  * @default 'zod'
38
101
  */
39
- importPath?: string
40
-
102
+ importPath?: 'zod' | 'zod/mini' | (string & {})
41
103
  /**
42
- * Choose to use date or datetime as JavaScript Date instead of string.
43
- * - false falls back to a simple z.string() format.
44
- * - 'string' uses z.string().datetime() for datetime validation.
45
- * - 'stringOffset' uses z.string().datetime({ offset: true }) for datetime with timezone offset validation.
46
- * - 'stringLocal' uses z.string().datetime({ local: true }) for local datetime validation.
47
- * - 'date' uses z.date() for JavaScript Date objects.
48
- * @default 'string'
49
- * @note 'stringOffset' will become the default in Kubb v3.
50
- */
51
- dateType?: false | 'string' | 'stringOffset' | 'stringLocal' | 'date'
52
- /**
53
- * Choose to use `number` or `bigint` for integer fields with `int64` format.
54
- * - 'number' uses the JavaScript `number` type (matches JSON.parse() runtime behavior).
55
- * - 'bigint' uses the JavaScript `bigint` type (accurate for values exceeding Number.MAX_SAFE_INTEGER).
56
- * @note in v5 of Kubb 'bigint' will become the default to better align with OpenAPI's int64 specification.
57
- * @default 'number'
58
- */
59
- integerType?: 'number' | 'bigint'
60
- /**
61
- * Which type to use when the Swagger/OpenAPI file is not providing more information.
62
- * - 'any' allows any value.
63
- * - 'unknown' requires type narrowing before use.
64
- * - 'void' represents no value.
65
- * @default 'any'
66
- */
67
- unknownType?: 'any' | 'unknown' | 'void'
68
- /**
69
- * Which type to use for empty schema values.
70
- * - 'any' allows any value.
71
- * - 'unknown' requires type narrowing before use.
72
- * - 'void' represents no value.
73
- * @default `unknownType`
74
- */
75
- emptySchemaType?: 'any' | 'unknown' | 'void'
76
- /**
77
- * Use TypeScript(`@kubb/plugin-ts`) to add type annotation.
104
+ * Add TypeScript type annotations to generated schemas.
78
105
  */
79
106
  typed?: boolean
80
107
  /**
81
- * Return Zod generated schema as type with z.infer<TYPE>
108
+ * Return schemas as inferred types using `z.infer`.
82
109
  */
83
110
  inferred?: boolean
84
111
  /**
85
- * Use of z.coerce.string() instead of z.string()
86
- * can also be an object to enable coercion for dates, strings, and numbers
112
+ * Apply coercion to string values or configure coercion per type.
87
113
  */
88
- coercion?:
89
- | boolean
90
- | {
91
- dates?: boolean
92
- strings?: boolean
93
- numbers?: boolean
94
- }
95
- operations?: boolean
96
- mapper?: Record<string, string>
97
- transformers?: {
98
- /**
99
- * Customize the names based on the type that is provided by the plugin.
100
- */
101
- name?: (name: ResolveNameParams['name'], type?: ResolveNameParams['type']) => string
102
- /**
103
- * Receive schema and baseName(propertyName) and return FakerMeta array
104
- * TODO TODO add docs
105
- * @beta
106
- */
107
- schema?: (
108
- props: {
109
- schema: SchemaObject | null
110
- name: string | null
111
- parentName: string | null
112
- },
113
- defaultSchemas: Schema[],
114
- ) => Schema[] | undefined
115
- }
114
+ coercion?: boolean | { dates?: boolean; strings?: boolean; numbers?: boolean }
116
115
  /**
117
- * Which version of Zod should be used.
118
- * - '3' uses Zod v3.x syntax and features.
119
- * - '4' uses Zod v4.x syntax and features.
120
- * @default '3'
116
+ * Generate operation-level schemas (grouped by operationId).
121
117
  */
122
- version?: '3' | '4'
118
+ operations?: boolean
123
119
  /**
124
- * Which Zod GUID validator to use for OpenAPI `format: uuid`.
125
- * - 'uuid' uses UUID validation.
126
- * - 'guid' uses GUID validation (Zod v4 only).
120
+ * Validator to use for UUID format: `uuid` or `guid`.
121
+ *
127
122
  * @default 'uuid'
128
123
  */
129
124
  guidType?: 'uuid' | 'guid'
130
125
  /**
131
- * Use Zod Mini's functional API for better tree-shaking support.
132
- * When enabled, generates functional syntax (e.g., `z.optional(z.string())`) instead of chainable methods (e.g., `z.string().optional()`).
133
- * Requires Zod v4 or later. When `mini: true`, `version` is set to '4' and `importPath` will default to 'zod/mini'.
126
+ * Use Zod Mini's functional API for better tree-shaking.
127
+ *
134
128
  * @default false
135
129
  */
136
130
  mini?: boolean
137
131
  /**
138
- * Callback function to wrap the output of the generated zod schema
132
+ * Callback to wrap the generated schema output.
139
133
  *
140
- * This is useful for edge case scenarios where you might leverage something like `z.object({ ... }).openapi({ example: { some: "complex-example" }})`
141
- * or `extendApi(z.object({ ... }), { example: { some: "complex-example", ...otherOpenApiProperties }})`
142
- * while going from openapi -> zod -> openapi
134
+ * Useful for adding metadata like `.openapi()` or extension helpers.
143
135
  */
144
- wrapOutput?: (arg: { output: string; schema: SchemaObject }) => string | undefined
136
+ wrapOutput?: (arg: { output: string; schema: ast.SchemaNode }) => string | undefined
145
137
  /**
146
- * Define some generators next to the zod generators
138
+ * Apply casing to parameter names.
139
+ */
140
+ paramsCasing?: 'camelcase'
141
+ /**
142
+ * Additional generators alongside the default generators.
147
143
  */
148
144
  generators?: Array<Generator<PluginZod>>
145
+ /**
146
+ * Override naming conventions for schema names and types.
147
+ */
148
+ resolver?: Partial<ResolverZod> & ThisType<ResolverZod>
149
+ /**
150
+ * Override printer node handlers to customize rendering of specific schema types.
151
+ */
152
+ printer?: {
153
+ nodes?: PrinterZodNodes | PrinterZodMiniNodes
154
+ }
155
+ /**
156
+ * AST visitor to transform schema and operation nodes.
157
+ */
158
+ transformer?: ast.Visitor
149
159
  }
150
160
 
151
161
  type ResolvedOptions = {
152
- output: Output<Oas>
153
- group: Options['group']
154
- override: NonNullable<Options['override']>
155
- transformers: NonNullable<Options['transformers']>
156
- dateType: NonNullable<Options['dateType']>
157
- integerType: NonNullable<Options['integerType']>
158
- unknownType: NonNullable<Options['unknownType']>
159
- emptySchemaType: NonNullable<Options['emptySchemaType']>
162
+ output: Output
163
+ exclude: Array<Exclude>
164
+ include: Array<Include> | undefined
165
+ override: Array<Override<ResolvedOptions>>
166
+ group: Group | undefined
160
167
  typed: NonNullable<Options['typed']>
161
168
  inferred: NonNullable<Options['inferred']>
162
- mapper: NonNullable<Options['mapper']>
163
169
  importPath: NonNullable<Options['importPath']>
164
170
  coercion: NonNullable<Options['coercion']>
165
171
  operations: NonNullable<Options['operations']>
166
- wrapOutput: Options['wrapOutput']
167
- version: NonNullable<Options['version']>
168
172
  guidType: NonNullable<Options['guidType']>
169
173
  mini: NonNullable<Options['mini']>
174
+ wrapOutput: Options['wrapOutput']
175
+ paramsCasing: Options['paramsCasing']
176
+ printer: Options['printer']
170
177
  }
171
178
 
172
- export type PluginZod = PluginFactoryOptions<'plugin-zod', Options, ResolvedOptions, never, ResolvePathOptions>
179
+ export type PluginZod = PluginFactoryOptions<'plugin-zod', Options, ResolvedOptions, ResolverZod>
180
+
181
+ declare global {
182
+ namespace Kubb {
183
+ interface PluginRegistry {
184
+ 'plugin-zod': PluginZod
185
+ }
186
+ }
187
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,222 @@
1
+ import { stringify, toRegExpString } from '@internals/utils'
2
+ import { ast } from '@kubb/core'
3
+ import type { PluginZod, ResolverZod } from './types.ts'
4
+
5
+ /**
6
+ * Returns `true` when the given coercion option enables coercion for the specified type.
7
+ */
8
+ export function shouldCoerce(coercion: PluginZod['resolvedOptions']['coercion'] | undefined, type: 'dates' | 'strings' | 'numbers'): boolean {
9
+ if (coercion === undefined || coercion === false) return false
10
+ if (coercion === true) return true
11
+
12
+ return !!coercion[type]
13
+ }
14
+
15
+ /**
16
+ * Collects all resolved schema names for an operation's parameters and responses
17
+ * into a single lookup object, useful for building imports and type references.
18
+ */
19
+ export function buildSchemaNames(node: ast.OperationNode, { params, resolver }: { params: Array<ast.ParameterNode>; resolver: ResolverZod }) {
20
+ const pathParam = params.find((p) => p.in === 'path')
21
+ const queryParam = params.find((p) => p.in === 'query')
22
+ const headerParam = params.find((p) => p.in === 'header')
23
+
24
+ const responses: Record<number | string, string> = {}
25
+ const errors: Record<number | string, string> = {}
26
+
27
+ for (const res of node.responses) {
28
+ const name = resolver.resolveResponseStatusName(node, res.statusCode)
29
+ const statusNum = Number(res.statusCode)
30
+
31
+ if (!Number.isNaN(statusNum)) {
32
+ responses[statusNum] = name
33
+ if (statusNum >= 400) {
34
+ errors[statusNum] = name
35
+ }
36
+ }
37
+ }
38
+
39
+ responses['default'] = resolver.resolveResponseName(node)
40
+
41
+ return {
42
+ request: node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : undefined,
43
+ 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,
47
+ },
48
+ responses,
49
+ errors,
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Format a default value as a code-level literal.
55
+ * Objects become `{}`, primitives become their string representation, strings are quoted.
56
+ */
57
+ export function formatDefault(value: unknown): string {
58
+ if (typeof value === 'string') return stringify(value)
59
+ if (typeof value === 'object' && value !== null) return '{}'
60
+
61
+ return String(value ?? '')
62
+ }
63
+
64
+ /**
65
+ * Format a primitive enum/literal value.
66
+ * Strings are quoted; numbers and booleans are emitted raw.
67
+ */
68
+ export function formatLiteral(v: string | number | boolean): string {
69
+ if (typeof v === 'string') return stringify(v)
70
+
71
+ return String(v)
72
+ }
73
+
74
+ /**
75
+ * Numeric constraint limits for Zod schemas (min, max, and exclusive bounds).
76
+ */
77
+ export type NumericConstraints = {
78
+ min?: number
79
+ max?: number
80
+ exclusiveMinimum?: number
81
+ exclusiveMaximum?: number
82
+ multipleOf?: number
83
+ }
84
+
85
+ /**
86
+ * Length constraint limits for string and array schemas (min, max, and regex pattern).
87
+ */
88
+ export type LengthConstraints = {
89
+ min?: number
90
+ max?: number
91
+ pattern?: string
92
+ }
93
+
94
+ /**
95
+ * Modifier options for applying chainable methods to Zod schema values.
96
+ */
97
+ export type ModifierOptions = {
98
+ value: string
99
+ nullable?: boolean
100
+ optional?: boolean
101
+ nullish?: boolean
102
+ defaultValue?: unknown
103
+ description?: string
104
+ }
105
+
106
+ /**
107
+ * Build `.min()` / `.max()` / `.gt()` / `.lt()` constraint chains for numbers
108
+ * using the standard chainable Zod v4 API.
109
+ */
110
+ export function numberConstraints({ min, max, exclusiveMinimum, exclusiveMaximum, multipleOf }: NumericConstraints): string {
111
+ return [
112
+ min !== undefined ? `.min(${min})` : '',
113
+ max !== undefined ? `.max(${max})` : '',
114
+ exclusiveMinimum !== undefined ? `.gt(${exclusiveMinimum})` : '',
115
+ exclusiveMaximum !== undefined ? `.lt(${exclusiveMaximum})` : '',
116
+ multipleOf !== undefined ? `.multipleOf(${multipleOf})` : '',
117
+ ].join('')
118
+ }
119
+
120
+ /**
121
+ * Build `.min()` / `.max()` / `.regex()` chains for strings/arrays
122
+ * using the standard chainable Zod v4 API.
123
+ */
124
+ export function lengthConstraints({ min, max, pattern }: LengthConstraints): string {
125
+ return [
126
+ min !== undefined ? `.min(${min})` : '',
127
+ max !== undefined ? `.max(${max})` : '',
128
+ pattern !== undefined ? `.regex(${toRegExpString(pattern, null)})` : '',
129
+ ].join('')
130
+ }
131
+
132
+ /**
133
+ * Build `.check(z.minimum(), z.maximum())` for `zod/mini` numeric constraints.
134
+ */
135
+ export function numberChecksMini({ min, max, exclusiveMinimum, exclusiveMaximum, multipleOf }: NumericConstraints): string {
136
+ const checks: string[] = []
137
+ if (min !== undefined) checks.push(`z.minimum(${min})`)
138
+ if (max !== undefined) checks.push(`z.maximum(${max})`)
139
+ if (exclusiveMinimum !== undefined) checks.push(`z.minimum(${exclusiveMinimum}, { exclusive: true })`)
140
+ if (exclusiveMaximum !== undefined) checks.push(`z.maximum(${exclusiveMaximum}, { exclusive: true })`)
141
+ if (multipleOf !== undefined) checks.push(`z.multipleOf(${multipleOf})`)
142
+ return checks.length ? `.check(${checks.join(', ')})` : ''
143
+ }
144
+
145
+ /**
146
+ * Build `.check(z.minLength(), z.maxLength(), z.regex())` for `zod/mini` length constraints.
147
+ */
148
+ export function lengthChecksMini({ min, max, pattern }: LengthConstraints): string {
149
+ const checks: string[] = []
150
+ if (min !== undefined) checks.push(`z.minLength(${min})`)
151
+ if (max !== undefined) checks.push(`z.maxLength(${max})`)
152
+ if (pattern !== undefined) checks.push(`z.regex(${toRegExpString(pattern, null)})`)
153
+ return checks.length ? `.check(${checks.join(', ')})` : ''
154
+ }
155
+
156
+ /**
157
+ * Apply nullable / optional / nullish modifiers and an optional `.describe()` call
158
+ * to a schema value string using the chainable Zod v4 API.
159
+ */
160
+ 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
176
+ }
177
+
178
+ /**
179
+ * Apply nullable / optional / nullish modifiers using the functional `zod/mini` API
180
+ * (`z.nullable()`, `z.optional()`, `z.nullish()`).
181
+ */
182
+ 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
198
+ }
199
+
200
+ type BuildGroupedParamsSchemaOptions = {
201
+ params: Array<ast.ParameterNode>
202
+ optional?: boolean
203
+ }
204
+
205
+ /**
206
+ * Builds an `object` schema node grouping the given parameter nodes.
207
+ * The `primitive: 'object'` marker ensures the Zod printer emits `z.object(…)` rather than a record.
208
+ */
209
+ export function buildGroupedParamsSchema({ params, optional }: BuildGroupedParamsSchemaOptions): ast.SchemaNode {
210
+ return ast.createSchema({
211
+ type: 'object',
212
+ optional,
213
+ primitive: 'object',
214
+ properties: params.map((param) =>
215
+ ast.createProperty({
216
+ name: param.name,
217
+ required: param.required,
218
+ schema: param.schema,
219
+ }),
220
+ ),
221
+ })
222
+ }