zod-codegen 1.6.1 → 1.6.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.
@@ -75,9 +75,13 @@ export class TypeScriptCodeGeneratorService implements CodeGenerator, SchemaBuil
75
75
  const serverConfig = this.buildServerConfiguration(openapi);
76
76
  const clientClass = this.buildClientClass(openapi, schemas);
77
77
 
78
+ const explicitTypeDeclarations = this.buildExplicitTypeDeclarations(openapi);
79
+
78
80
  return [
79
81
  this.createComment('Imports'),
80
82
  ...imports,
83
+ this.createComment('Explicit type declarations'),
84
+ ...explicitTypeDeclarations,
81
85
  this.createComment('Components schemas'),
82
86
  ...Object.values(schemas),
83
87
  ...schemaTypeAliases,
@@ -108,14 +112,22 @@ export class TypeScriptCodeGeneratorService implements CodeGenerator, SchemaBuil
108
112
  // Clear context
109
113
  this.currentSchemaName = null;
110
114
 
115
+ const sanitizedName = this.typeBuilder.sanitizeIdentifier(name);
116
+
117
+ // Add type annotation: z.ZodType<Name>
118
+ const typeAnnotation = ts.factory.createTypeReferenceNode(
119
+ ts.factory.createQualifiedName(ts.factory.createIdentifier('z'), ts.factory.createIdentifier('ZodType')),
120
+ [ts.factory.createTypeReferenceNode(ts.factory.createIdentifier(sanitizedName), undefined)],
121
+ );
122
+
111
123
  const variableStatement = ts.factory.createVariableStatement(
112
124
  [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
113
125
  ts.factory.createVariableDeclarationList(
114
126
  [
115
127
  ts.factory.createVariableDeclaration(
116
- ts.factory.createIdentifier(this.typeBuilder.sanitizeIdentifier(name)),
117
- undefined,
128
+ ts.factory.createIdentifier(sanitizedName),
118
129
  undefined,
130
+ typeAnnotation,
119
131
  schemaExpression,
120
132
  ),
121
133
  ],
@@ -130,19 +142,218 @@ export class TypeScriptCodeGeneratorService implements CodeGenerator, SchemaBuil
130
142
  }, {});
131
143
  }
132
144
 
133
- private buildSchemaTypeAliases(schemas: Record<string, ts.VariableStatement>): ts.TypeAliasDeclaration[] {
134
- return Object.keys(schemas).map((name) => {
145
+ private buildSchemaTypeAliases(_schemas: Record<string, ts.VariableStatement>): ts.TypeAliasDeclaration[] {
146
+ // Explicit type declarations are used instead of z.infer type exports
147
+ return [];
148
+ }
149
+
150
+ /**
151
+ * Builds explicit TypeScript type declarations for all schemas.
152
+ * Returns interface declarations for object types and type aliases for other types.
153
+ */
154
+ private buildExplicitTypeDeclarations(openapi: OpenApiSpecType): ts.Statement[] {
155
+ const schemasEntries = Object.entries(openapi.components?.schemas ?? {});
156
+ const schemasMap = Object.fromEntries(schemasEntries);
157
+ const sortedSchemaNames = this.topologicalSort(schemasMap);
158
+
159
+ const statements: ts.Statement[] = [];
160
+
161
+ for (const name of sortedSchemaNames) {
162
+ const schema = openapi.components?.schemas?.[name];
163
+ if (!schema) continue;
164
+
135
165
  const sanitizedName = this.typeBuilder.sanitizeIdentifier(name);
136
- return ts.factory.createTypeAliasDeclaration(
137
- [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
138
- ts.factory.createIdentifier(sanitizedName),
139
- undefined,
140
- ts.factory.createTypeReferenceNode(
141
- ts.factory.createQualifiedName(ts.factory.createIdentifier('z'), ts.factory.createIdentifier('infer')),
142
- [ts.factory.createTypeQueryNode(ts.factory.createIdentifier(sanitizedName), undefined)],
166
+ const safeSchema = SchemaProperties.safeParse(schema);
167
+
168
+ if (!safeSchema.success) {
169
+ // Unknown schema type, create a type alias to unknown
170
+ statements.push(
171
+ ts.factory.createTypeAliasDeclaration(
172
+ [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
173
+ ts.factory.createIdentifier(sanitizedName),
174
+ undefined,
175
+ ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword),
176
+ ),
177
+ );
178
+ continue;
179
+ }
180
+
181
+ const schemaData = safeSchema.data;
182
+ const typeNode = this.buildTypeNode(schemaData);
183
+
184
+ // For object types with properties, create an interface
185
+ if (schemaData['type'] === 'object' && schemaData['properties']) {
186
+ statements.push(this.buildInterfaceDeclaration(sanitizedName, schemaData));
187
+ continue;
188
+ }
189
+
190
+ // For all other types (enums, arrays, unions, etc.), create a type alias
191
+ statements.push(
192
+ ts.factory.createTypeAliasDeclaration(
193
+ [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
194
+ ts.factory.createIdentifier(sanitizedName),
195
+ undefined,
196
+ typeNode,
143
197
  ),
144
198
  );
199
+ }
200
+
201
+ return statements;
202
+ }
203
+
204
+ /**
205
+ * Converts an OpenAPI schema to a TypeScript type node.
206
+ */
207
+ private buildTypeNode(schema: unknown): ts.TypeNode {
208
+ const safeSchema = SchemaProperties.safeParse(schema);
209
+ if (!safeSchema.success) {
210
+ return ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword);
211
+ }
212
+
213
+ const prop = safeSchema.data;
214
+
215
+ // Handle $ref
216
+ if (this.isReference(prop)) {
217
+ const {$ref = ''} = Reference.parse(prop);
218
+ const refName = $ref.split('/').pop() ?? 'never';
219
+ const sanitizedRefName = this.typeBuilder.sanitizeIdentifier(refName);
220
+ return ts.factory.createTypeReferenceNode(ts.factory.createIdentifier(sanitizedRefName), undefined);
221
+ }
222
+
223
+ // Handle nullable
224
+ const isNullable = prop['nullable'] === true;
225
+ const baseTypeNode = this.buildBaseTypeNode(prop);
226
+
227
+ if (isNullable) {
228
+ return ts.factory.createUnionTypeNode([baseTypeNode, ts.factory.createLiteralTypeNode(ts.factory.createNull())]);
229
+ }
230
+
231
+ return baseTypeNode;
232
+ }
233
+
234
+ /**
235
+ * Builds the base type node without nullable handling.
236
+ */
237
+ private buildBaseTypeNode(prop: Record<string, unknown>): ts.TypeNode {
238
+ // Handle anyOf/oneOf (union types)
239
+ if (prop['anyOf'] && Array.isArray(prop['anyOf']) && prop['anyOf'].length > 0) {
240
+ const types = prop['anyOf'].map((s: unknown) => this.buildTypeNode(s));
241
+ return ts.factory.createUnionTypeNode(types);
242
+ }
243
+
244
+ if (prop['oneOf'] && Array.isArray(prop['oneOf']) && prop['oneOf'].length > 0) {
245
+ const types = prop['oneOf'].map((s: unknown) => this.buildTypeNode(s));
246
+ return ts.factory.createUnionTypeNode(types);
247
+ }
248
+
249
+ // Handle allOf (intersection types)
250
+ if (prop['allOf'] && Array.isArray(prop['allOf']) && prop['allOf'].length > 0) {
251
+ const types = prop['allOf'].map((s: unknown) => this.buildTypeNode(s));
252
+ return ts.factory.createIntersectionTypeNode(types);
253
+ }
254
+
255
+ // Handle enum
256
+ if (prop['enum'] && Array.isArray(prop['enum']) && prop['enum'].length > 0) {
257
+ const literalTypes = prop['enum'].map((val: unknown) => {
258
+ if (typeof val === 'string') {
259
+ return ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(val, true));
260
+ } else if (typeof val === 'number') {
261
+ if (val < 0) {
262
+ return ts.factory.createLiteralTypeNode(
263
+ ts.factory.createPrefixUnaryExpression(
264
+ ts.SyntaxKind.MinusToken,
265
+ ts.factory.createNumericLiteral(String(Math.abs(val))),
266
+ ),
267
+ );
268
+ }
269
+ return ts.factory.createLiteralTypeNode(ts.factory.createNumericLiteral(String(val)));
270
+ } else if (typeof val === 'boolean') {
271
+ return ts.factory.createLiteralTypeNode(val ? ts.factory.createTrue() : ts.factory.createFalse());
272
+ }
273
+ return ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword);
274
+ });
275
+ return ts.factory.createUnionTypeNode(literalTypes);
276
+ }
277
+
278
+ // Handle type-specific schemas
279
+ switch (prop['type']) {
280
+ case 'string':
281
+ return ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
282
+ case 'number':
283
+ case 'integer':
284
+ return ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword);
285
+ case 'boolean':
286
+ return ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword);
287
+ case 'array': {
288
+ const itemsType = prop['items']
289
+ ? this.buildTypeNode(prop['items'])
290
+ : ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword);
291
+ return ts.factory.createArrayTypeNode(itemsType);
292
+ }
293
+ case 'object': {
294
+ const properties = (prop['properties'] ?? {}) as Record<string, unknown>;
295
+ const requiredProps = (prop['required'] ?? []) as string[];
296
+
297
+ if (Object.keys(properties).length > 0) {
298
+ return this.buildObjectTypeLiteral(properties, requiredProps);
299
+ }
300
+
301
+ // Empty object or additionalProperties - use Record<string, unknown>
302
+ return ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('Record'), [
303
+ ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
304
+ ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword),
305
+ ]);
306
+ }
307
+ default:
308
+ return ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword);
309
+ }
310
+ }
311
+
312
+ /**
313
+ * Builds a TypeScript type literal for an object schema.
314
+ */
315
+ private buildObjectTypeLiteral(properties: Record<string, unknown>, requiredProps: string[]): ts.TypeLiteralNode {
316
+ const members = Object.entries(properties).map(([name, propSchema]) => {
317
+ const isRequired = requiredProps.includes(name);
318
+ const typeNode = this.buildTypeNode(propSchema);
319
+
320
+ return ts.factory.createPropertySignature(
321
+ undefined,
322
+ ts.factory.createIdentifier(name),
323
+ isRequired ? undefined : ts.factory.createToken(ts.SyntaxKind.QuestionToken),
324
+ typeNode,
325
+ );
326
+ });
327
+
328
+ return ts.factory.createTypeLiteralNode(members);
329
+ }
330
+
331
+ /**
332
+ * Builds a TypeScript interface declaration for an object schema.
333
+ */
334
+ private buildInterfaceDeclaration(name: string, schema: Record<string, unknown>): ts.InterfaceDeclaration {
335
+ const properties = (schema['properties'] ?? {}) as Record<string, unknown>;
336
+ const requiredProps = (schema['required'] ?? []) as string[];
337
+
338
+ const members = Object.entries(properties).map(([propName, propSchema]) => {
339
+ const isRequired = requiredProps.includes(propName);
340
+ const typeNode = this.buildTypeNode(propSchema);
341
+
342
+ return ts.factory.createPropertySignature(
343
+ undefined,
344
+ ts.factory.createIdentifier(propName),
345
+ isRequired ? undefined : ts.factory.createToken(ts.SyntaxKind.QuestionToken),
346
+ typeNode,
347
+ );
145
348
  });
349
+
350
+ return ts.factory.createInterfaceDeclaration(
351
+ [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
352
+ ts.factory.createIdentifier(name),
353
+ undefined,
354
+ undefined,
355
+ members,
356
+ );
146
357
  }
147
358
 
148
359
  private buildClientClass(
@@ -336,7 +547,7 @@ export class TypeScriptCodeGeneratorService implements CodeGenerator, SchemaBuil
336
547
  ]),
337
548
  ts.factory.createBlock(
338
549
  [
339
- // Build URL with query parameters
550
+ // Create initial URL object that we will use to build the final URL
340
551
  ts.factory.createVariableStatement(
341
552
  undefined,
342
553
  ts.factory.createVariableDeclarationList(
@@ -345,17 +556,11 @@ export class TypeScriptCodeGeneratorService implements CodeGenerator, SchemaBuil
345
556
  ts.factory.createIdentifier('baseUrl'),
346
557
  undefined,
347
558
  undefined,
348
- ts.factory.createTemplateExpression(ts.factory.createTemplateHead('', ''), [
349
- ts.factory.createTemplateSpan(
350
- ts.factory.createPropertyAccessExpression(
351
- ts.factory.createThis(),
352
- ts.factory.createPrivateIdentifier('#baseUrl'),
353
- ),
354
- ts.factory.createTemplateMiddle('', ''),
355
- ),
356
- ts.factory.createTemplateSpan(
357
- ts.factory.createIdentifier('path'),
358
- ts.factory.createTemplateTail('', ''),
559
+ ts.factory.createNewExpression(ts.factory.createIdentifier('URL'), undefined, [
560
+ ts.factory.createIdentifier('path'),
561
+ ts.factory.createPropertyAccessExpression(
562
+ ts.factory.createThis(),
563
+ ts.factory.createPrivateIdentifier('#baseUrl'),
359
564
  ),
360
565
  ]),
361
566
  ),
@@ -400,121 +605,155 @@ export class TypeScriptCodeGeneratorService implements CodeGenerator, SchemaBuil
400
605
  ),
401
606
  ),
402
607
  undefined,
403
- (() => {
404
- const urlObj = ts.factory.createNewExpression(ts.factory.createIdentifier('URL'), undefined, [
405
- ts.factory.createIdentifier('baseUrl'),
406
- ]);
407
- const forEachCall = ts.factory.createCallExpression(
408
- ts.factory.createPropertyAccessExpression(
409
- ts.factory.createCallExpression(
410
- ts.factory.createPropertyAccessExpression(
411
- ts.factory.createIdentifier('Object'),
412
- ts.factory.createIdentifier('entries'),
413
- ),
414
- undefined,
415
- [
416
- ts.factory.createPropertyAccessExpression(
417
- ts.factory.createIdentifier('options'),
418
- ts.factory.createIdentifier('params'),
419
- ),
420
- ],
421
- ),
422
- ts.factory.createIdentifier('forEach'),
423
- ),
424
- undefined,
425
- [
426
- ts.factory.createArrowFunction(
427
- undefined,
428
- undefined,
608
+ ts.factory.createCallExpression(
609
+ ts.factory.createParenthesizedExpression(
610
+ ts.factory.createArrowFunction(
611
+ undefined,
612
+ undefined,
613
+ [],
614
+ undefined,
615
+ ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
616
+ ts.factory.createBlock(
429
617
  [
430
- ts.factory.createParameterDeclaration(
431
- undefined,
432
- undefined,
433
- ts.factory.createArrayBindingPattern([
434
- ts.factory.createBindingElement(
435
- undefined,
436
- undefined,
437
- ts.factory.createIdentifier('key'),
438
- undefined,
439
- ),
440
- ts.factory.createBindingElement(
441
- undefined,
442
- undefined,
443
- ts.factory.createIdentifier('value'),
444
- undefined,
445
- ),
446
- ]),
447
- undefined,
448
- undefined,
449
- ),
450
- ],
451
- undefined,
452
- ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
453
- ts.factory.createBlock(
454
- [
455
- ts.factory.createExpressionStatement(
456
- ts.factory.createCallExpression(
457
- ts.factory.createPropertyAccessExpression(
618
+ ts.factory.createExpressionStatement(
619
+ ts.factory.createCallExpression(
620
+ ts.factory.createPropertyAccessExpression(
621
+ ts.factory.createCallExpression(
458
622
  ts.factory.createPropertyAccessExpression(
459
- urlObj,
460
- ts.factory.createIdentifier('searchParams'),
623
+ ts.factory.createCallExpression(
624
+ ts.factory.createPropertyAccessExpression(
625
+ ts.factory.createIdentifier('Object'),
626
+ ts.factory.createIdentifier('entries'),
627
+ ),
628
+ undefined,
629
+ [
630
+ ts.factory.createPropertyAccessExpression(
631
+ ts.factory.createIdentifier('options'),
632
+ ts.factory.createIdentifier('params'),
633
+ ),
634
+ ],
635
+ ),
636
+ ts.factory.createIdentifier('filter'),
461
637
  ),
462
- ts.factory.createIdentifier('set'),
638
+ undefined,
639
+ [
640
+ ts.factory.createArrowFunction(
641
+ undefined,
642
+ undefined,
643
+ [
644
+ ts.factory.createParameterDeclaration(
645
+ undefined,
646
+ undefined,
647
+ ts.factory.createArrayBindingPattern([
648
+ ts.factory.createBindingElement(
649
+ undefined,
650
+ undefined,
651
+ ts.factory.createIdentifier(''),
652
+ undefined,
653
+ ),
654
+ ts.factory.createBindingElement(
655
+ undefined,
656
+ undefined,
657
+ ts.factory.createIdentifier('value'),
658
+ undefined,
659
+ ),
660
+ ]),
661
+ undefined,
662
+ undefined,
663
+ ),
664
+ ],
665
+ undefined,
666
+ ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
667
+ ts.factory.createBinaryExpression(
668
+ ts.factory.createIdentifier('value'),
669
+ ts.SyntaxKind.ExclamationEqualsEqualsToken,
670
+ ts.factory.createIdentifier('undefined'),
671
+ ),
672
+ ),
673
+ ],
463
674
  ),
464
- undefined,
465
- [
466
- ts.factory.createIdentifier('key'),
467
- ts.factory.createCallExpression(
468
- ts.factory.createIdentifier('String'),
469
- undefined,
470
- [ts.factory.createIdentifier('value')],
471
- ),
472
- ],
675
+ ts.factory.createIdentifier('forEach'),
473
676
  ),
474
- ),
475
- ],
476
- false,
477
- ),
478
- ),
479
- ],
480
- );
481
- // Use IIFE to execute forEach and return URL string
482
- return ts.factory.createCallExpression(
483
- ts.factory.createParenthesizedExpression(
484
- ts.factory.createArrowFunction(
485
- undefined,
486
- undefined,
487
- [],
488
- undefined,
489
- ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
490
- ts.factory.createBlock(
491
- [
492
- ts.factory.createExpressionStatement(forEachCall),
493
- ts.factory.createReturnStatement(
494
- ts.factory.createCallExpression(
495
- ts.factory.createPropertyAccessExpression(
496
- urlObj,
497
- ts.factory.createIdentifier('toString'),
677
+ undefined,
678
+ [
679
+ ts.factory.createArrowFunction(
680
+ undefined,
681
+ undefined,
682
+ [
683
+ ts.factory.createParameterDeclaration(
684
+ undefined,
685
+ undefined,
686
+ ts.factory.createArrayBindingPattern([
687
+ ts.factory.createBindingElement(
688
+ undefined,
689
+ undefined,
690
+ ts.factory.createIdentifier('key'),
691
+ undefined,
692
+ ),
693
+ ts.factory.createBindingElement(
694
+ undefined,
695
+ undefined,
696
+ ts.factory.createIdentifier('value'),
697
+ undefined,
698
+ ),
699
+ ]),
700
+ undefined,
701
+ undefined,
702
+ ),
703
+ ],
704
+ undefined,
705
+ ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
706
+ ts.factory.createBlock(
707
+ [
708
+ ts.factory.createExpressionStatement(
709
+ ts.factory.createCallExpression(
710
+ ts.factory.createPropertyAccessExpression(
711
+ ts.factory.createPropertyAccessExpression(
712
+ ts.factory.createIdentifier('baseUrl'),
713
+ ts.factory.createIdentifier('searchParams'),
714
+ ),
715
+ ts.factory.createIdentifier('set'),
716
+ ),
717
+ undefined,
718
+ [
719
+ ts.factory.createIdentifier('key'),
720
+ ts.factory.createCallExpression(
721
+ ts.factory.createIdentifier('String'),
722
+ undefined,
723
+ [ts.factory.createIdentifier('value')],
724
+ ),
725
+ ],
726
+ ),
727
+ ),
728
+ ],
729
+ false,
730
+ ),
498
731
  ),
499
- undefined,
500
- [],
732
+ ],
733
+ ),
734
+ ),
735
+ ts.factory.createReturnStatement(
736
+ ts.factory.createCallExpression(
737
+ ts.factory.createPropertyAccessExpression(
738
+ ts.factory.createIdentifier('baseUrl'),
739
+ ts.factory.createIdentifier('toString'),
501
740
  ),
741
+ undefined,
742
+ [],
502
743
  ),
503
- ],
504
- false,
505
- ),
744
+ ),
745
+ ],
746
+ true,
506
747
  ),
507
748
  ),
508
- undefined,
509
- [],
510
- );
511
- })(),
749
+ ),
750
+ undefined,
751
+ [],
752
+ ),
512
753
  undefined,
513
754
  ts.factory.createCallExpression(
514
755
  ts.factory.createPropertyAccessExpression(
515
- ts.factory.createNewExpression(ts.factory.createIdentifier('URL'), undefined, [
516
- ts.factory.createIdentifier('baseUrl'),
517
- ]),
756
+ ts.factory.createIdentifier('baseUrl'),
518
757
  ts.factory.createIdentifier('toString'),
519
758
  ),
520
759
  undefined,