zod-codegen 1.1.0 → 1.1.1

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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## <small>1.1.1 (2025-11-13)</small>
2
+
3
+ - Merge pull request #29 from julienandreu/feat/server-configuration-and-examples ([c598b5a](https://github.com/julienandreu/zod-codegen/commit/c598b5a)), closes [#29](https://github.com/julienandreu/zod-codegen/issues/29)
4
+ - fix: clarify getBaseRequestOptions merging behavior ([13379fd](https://github.com/julienandreu/zod-codegen/commit/13379fd))
5
+ - fix: use z.union with z.literal for numeric enums instead of z.enum ([5e0c7ea](https://github.com/julienandreu/zod-codegen/commit/5e0c7ea))
6
+
1
7
  ## 1.1.0 (2025-11-13)
2
8
 
3
9
  - Merge pull request #27 from julienandreu/docs/update-readme-reflect-changes ([3f6745e](https://github.com/julienandreu/zod-codegen/commit/3f6745e)), closes [#27](https://github.com/julienandreu/zod-codegen/issues/27)
package/EXAMPLES.md CHANGED
@@ -405,10 +405,17 @@ The generated client uses a layered approach to request configuration:
405
405
 
406
406
  When a request is made, options are merged in this order (later values override earlier ones):
407
407
 
408
- 1. **Base Options** from `getBaseRequestOptions()` (headers, signal, credentials, etc.)
409
- 2. **Content-Type Header** (automatically set based on request body: `application/json` or `application/x-www-form-urlencoded`)
410
- 3. **Request-Specific Headers** from `options.headers` parameter (if provided)
411
- 4. **Method and Body** (always set by generated code, cannot be overridden)
408
+ 1. **Base Options** from `getBaseRequestOptions()` - All RequestInit options (headers, signal, credentials, mode, cache, etc.)
409
+ 2. **Content-Type Header** - Automatically set based on request body (`application/json` or `application/x-www-form-urlencoded`)
410
+ 3. **Request-Specific Headers** - From `options.headers` parameter (if provided)
411
+ 4. **Method and Body** - Always set by generated code (cannot be overridden)
412
+
413
+ **Important**: `getBaseRequestOptions()` returns **base options that are merged with**, not replaced by, request-specific options. This means:
414
+
415
+ - ✅ Base options like `mode`, `credentials`, `signal` are preserved
416
+ - ✅ Headers are merged (base headers + Content-Type + request headers)
417
+ - ✅ Request-specific headers override base headers
418
+ - ✅ Method and body always come from the request (not from baseOptions)
412
419
 
413
420
  ### Type Safety
414
421
 
@@ -419,20 +426,39 @@ The `getBaseRequestOptions()` method returns `Partial<Omit<RequestInit, 'method'
419
426
 
420
427
  This ensures type safety while preventing accidental overrides of critical request properties.
421
428
 
422
- ### Header Merging Details
429
+ ### Complete Options Merging Details
423
430
 
424
- Headers are merged using `Object.assign()` with this priority:
431
+ The final fetch request uses `Object.assign()` to merge options:
425
432
 
426
433
  ```typescript
434
+ // Headers are merged first:
427
435
  const finalHeaders = Object.assign(
428
436
  {}, // Start with empty object
429
437
  baseOptions.headers || {}, // 1. Base headers from getBaseRequestOptions()
430
438
  {'Content-Type': contentType}, // 2. Content-Type (may override base)
431
439
  options.headers || {}, // 3. Request-specific headers (highest priority)
432
440
  );
441
+
442
+ // Then all options are merged:
443
+ const finalOptions = Object.assign(
444
+ {}, // Start with empty object
445
+ baseOptions, // 1. All base options (mode, credentials, signal, cache, etc.)
446
+ {
447
+ // 2. Request-specific options (override base)
448
+ method, // Always from endpoint
449
+ headers: finalHeaders, // Merged headers
450
+ body, // Always from request data
451
+ },
452
+ );
453
+
454
+ fetch(url, finalOptions);
433
455
  ```
434
456
 
435
- **Important**: Always return `Record<string, string>` for headers in `getBaseRequestOptions()` for predictable merging behavior.
457
+ **Important**:
458
+
459
+ - Always return `Record<string, string>` for headers in `getBaseRequestOptions()` for predictable merging behavior
460
+ - Base options (like `mode`, `credentials`, `signal`) are preserved unless explicitly overridden
461
+ - Headers are merged, not replaced - base headers + Content-Type + request headers
436
462
 
437
463
  ### Request Flow
438
464
 
package/README.md CHANGED
@@ -253,6 +253,8 @@ The generated client includes a protected `getBaseRequestOptions()` method that
253
253
  - **CORS**: `mode`, `credentials` for cross-origin requests
254
254
  - **Request Options**: `signal` (AbortController), `cache`, `redirect`, `referrer`, etc.
255
255
 
256
+ **Important**: Options from `getBaseRequestOptions()` are **merged with** (not replaced by) request-specific options. Base options like `mode`, `credentials`, and `signal` are preserved, while headers are merged (base headers + Content-Type + request headers). See [EXAMPLES.md](EXAMPLES.md) for detailed merging behavior.
257
+
256
258
  #### Basic Authentication Example
257
259
 
258
260
  ```typescript
@@ -708,26 +708,45 @@ export class TypeScriptCodeGeneratorService {
708
708
  }
709
709
  // Handle enum
710
710
  if (prop['enum'] && Array.isArray(prop['enum']) && prop['enum'].length > 0) {
711
- const enumValues = prop['enum'].map((val) => {
712
- if (typeof val === 'string') {
713
- return ts.factory.createStringLiteral(val, true);
714
- }
715
- if (typeof val === 'number') {
716
- // Handle negative numbers correctly
717
- if (val < 0) {
718
- return ts.factory.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, ts.factory.createNumericLiteral(String(Math.abs(val))));
711
+ // Check if all enum values are strings (z.enum only works with strings)
712
+ const allStrings = prop['enum'].every((val) => typeof val === 'string');
713
+ if (allStrings) {
714
+ // Use z.enum() for string enums
715
+ const enumValues = prop['enum'].map((val) => ts.factory.createStringLiteral(val, true));
716
+ const enumExpression = ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('z'), ts.factory.createIdentifier('enum')), undefined, [ts.factory.createArrayLiteralExpression(enumValues, false)]);
717
+ return required
718
+ ? enumExpression
719
+ : ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(enumExpression, ts.factory.createIdentifier('optional')), undefined, []);
720
+ }
721
+ else {
722
+ // Use z.union([z.literal(...), ...]) for numeric/boolean/mixed enums
723
+ const literalSchemas = prop['enum'].map((val) => {
724
+ let literalValue;
725
+ if (typeof val === 'string') {
726
+ literalValue = ts.factory.createStringLiteral(val, true);
719
727
  }
720
- return ts.factory.createNumericLiteral(String(val));
721
- }
722
- if (typeof val === 'boolean') {
723
- return val ? ts.factory.createTrue() : ts.factory.createFalse();
724
- }
725
- return ts.factory.createStringLiteral(String(val), true);
726
- });
727
- const enumExpression = ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('z'), ts.factory.createIdentifier('enum')), undefined, [ts.factory.createArrayLiteralExpression(enumValues, false)]);
728
- return required
729
- ? enumExpression
730
- : ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(enumExpression, ts.factory.createIdentifier('optional')), undefined, []);
728
+ else if (typeof val === 'number') {
729
+ // Handle negative numbers correctly
730
+ if (val < 0) {
731
+ literalValue = ts.factory.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, ts.factory.createNumericLiteral(String(Math.abs(val))));
732
+ }
733
+ else {
734
+ literalValue = ts.factory.createNumericLiteral(String(val));
735
+ }
736
+ }
737
+ else if (typeof val === 'boolean') {
738
+ literalValue = val ? ts.factory.createTrue() : ts.factory.createFalse();
739
+ }
740
+ else {
741
+ literalValue = ts.factory.createStringLiteral(String(val), true);
742
+ }
743
+ return ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('z'), ts.factory.createIdentifier('literal')), undefined, [literalValue]);
744
+ });
745
+ const unionExpression = ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('z'), ts.factory.createIdentifier('union')), undefined, [ts.factory.createArrayLiteralExpression(literalSchemas, false)]);
746
+ return required
747
+ ? unionExpression
748
+ : ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(unionExpression, ts.factory.createIdentifier('optional')), undefined, []);
749
+ }
731
750
  }
732
751
  switch (prop['type']) {
733
752
  case 'array': {
@@ -167,6 +167,77 @@ describe('TypeScriptCodeGeneratorService', () => {
167
167
  expect(code).toContain('inactive');
168
168
  expect(code).toContain('pending');
169
169
  });
170
+ it('should handle numeric enum types with z.union and z.literal', () => {
171
+ const spec = {
172
+ openapi: '3.0.0',
173
+ info: {
174
+ title: 'Test API',
175
+ version: '1.0.0',
176
+ },
177
+ paths: {},
178
+ components: {
179
+ schemas: {
180
+ Status: {
181
+ type: 'integer',
182
+ enum: [-99, 0, 1, 2],
183
+ },
184
+ ExecutionMode: {
185
+ type: 'integer',
186
+ enum: [1, 2],
187
+ },
188
+ },
189
+ },
190
+ };
191
+ const code = generator.generate(spec);
192
+ // Numeric enums should use z.union([z.literal(...), ...])
193
+ expect(code).toContain('z.union');
194
+ expect(code).toContain('z.literal');
195
+ expect(code).toContain('-99');
196
+ expect(code).toContain('0');
197
+ expect(code).toContain('1');
198
+ expect(code).toContain('2');
199
+ // Should not use z.enum for numeric enums
200
+ expect(code).not.toContain('Status: z.enum');
201
+ expect(code).not.toContain('ExecutionMode: z.enum');
202
+ });
203
+ it('should merge baseOptions with request-specific options in #makeRequest', () => {
204
+ const spec = {
205
+ openapi: '3.0.0',
206
+ info: {
207
+ title: 'Test API',
208
+ version: '1.0.0',
209
+ },
210
+ paths: {
211
+ '/test': {
212
+ get: {
213
+ operationId: 'testEndpoint',
214
+ responses: {
215
+ '200': {
216
+ description: 'Success',
217
+ content: {
218
+ 'application/json': {
219
+ schema: { type: 'string' },
220
+ },
221
+ },
222
+ },
223
+ },
224
+ },
225
+ },
226
+ },
227
+ };
228
+ const code = generator.generate(spec);
229
+ // Should call getBaseRequestOptions()
230
+ expect(code).toContain('getBaseRequestOptions()');
231
+ // Should merge headers: baseHeaders + Content-Type + request headers
232
+ expect(code).toContain('Object.assign');
233
+ expect(code).toContain('baseHeaders');
234
+ expect(code).toContain('Content-Type');
235
+ // Should merge all options: baseOptions + {method, headers, body}
236
+ expect(code).toMatch(/Object\.assign\s*\(\s*\{\s*\}\s*,\s*baseOptions/);
237
+ expect(code).toContain('method');
238
+ expect(code).toContain('headers');
239
+ expect(code).toContain('body');
240
+ });
170
241
  it('should handle array types', () => {
171
242
  const spec = {
172
243
  openapi: '3.0.0',
package/package.json CHANGED
@@ -100,5 +100,5 @@
100
100
  "release": "semantic-release",
101
101
  "release:dry": "semantic-release --dry-run"
102
102
  },
103
- "version": "1.1.0"
103
+ "version": "1.1.1"
104
104
  }
@@ -1750,42 +1750,77 @@ export class TypeScriptCodeGeneratorService implements CodeGenerator, SchemaBuil
1750
1750
 
1751
1751
  // Handle enum
1752
1752
  if (prop['enum'] && Array.isArray(prop['enum']) && prop['enum'].length > 0) {
1753
- const enumValues = prop['enum'].map((val) => {
1754
- if (typeof val === 'string') {
1755
- return ts.factory.createStringLiteral(val, true);
1756
- }
1757
- if (typeof val === 'number') {
1758
- // Handle negative numbers correctly
1759
- if (val < 0) {
1760
- return ts.factory.createPrefixUnaryExpression(
1761
- ts.SyntaxKind.MinusToken,
1762
- ts.factory.createNumericLiteral(String(Math.abs(val))),
1753
+ // Check if all enum values are strings (z.enum only works with strings)
1754
+ const allStrings = prop['enum'].every((val) => typeof val === 'string');
1755
+
1756
+ if (allStrings) {
1757
+ // Use z.enum() for string enums
1758
+ const enumValues = prop['enum'].map((val) => ts.factory.createStringLiteral(val as string, true));
1759
+ const enumExpression = ts.factory.createCallExpression(
1760
+ ts.factory.createPropertyAccessExpression(
1761
+ ts.factory.createIdentifier('z'),
1762
+ ts.factory.createIdentifier('enum'),
1763
+ ),
1764
+ undefined,
1765
+ [ts.factory.createArrayLiteralExpression(enumValues, false)],
1766
+ );
1767
+
1768
+ return required
1769
+ ? enumExpression
1770
+ : ts.factory.createCallExpression(
1771
+ ts.factory.createPropertyAccessExpression(enumExpression, ts.factory.createIdentifier('optional')),
1772
+ undefined,
1773
+ [],
1763
1774
  );
1775
+ } else {
1776
+ // Use z.union([z.literal(...), ...]) for numeric/boolean/mixed enums
1777
+ const literalSchemas = prop['enum'].map((val) => {
1778
+ let literalValue: ts.Expression;
1779
+ if (typeof val === 'string') {
1780
+ literalValue = ts.factory.createStringLiteral(val, true);
1781
+ } else if (typeof val === 'number') {
1782
+ // Handle negative numbers correctly
1783
+ if (val < 0) {
1784
+ literalValue = ts.factory.createPrefixUnaryExpression(
1785
+ ts.SyntaxKind.MinusToken,
1786
+ ts.factory.createNumericLiteral(String(Math.abs(val))),
1787
+ );
1788
+ } else {
1789
+ literalValue = ts.factory.createNumericLiteral(String(val));
1790
+ }
1791
+ } else if (typeof val === 'boolean') {
1792
+ literalValue = val ? ts.factory.createTrue() : ts.factory.createFalse();
1793
+ } else {
1794
+ literalValue = ts.factory.createStringLiteral(String(val), true);
1764
1795
  }
1765
- return ts.factory.createNumericLiteral(String(val));
1766
- }
1767
- if (typeof val === 'boolean') {
1768
- return val ? ts.factory.createTrue() : ts.factory.createFalse();
1769
- }
1770
- return ts.factory.createStringLiteral(String(val), true);
1771
- });
1772
1796
 
1773
- const enumExpression = ts.factory.createCallExpression(
1774
- ts.factory.createPropertyAccessExpression(
1775
- ts.factory.createIdentifier('z'),
1776
- ts.factory.createIdentifier('enum'),
1777
- ),
1778
- undefined,
1779
- [ts.factory.createArrayLiteralExpression(enumValues, false)],
1780
- );
1781
-
1782
- return required
1783
- ? enumExpression
1784
- : ts.factory.createCallExpression(
1785
- ts.factory.createPropertyAccessExpression(enumExpression, ts.factory.createIdentifier('optional')),
1797
+ return ts.factory.createCallExpression(
1798
+ ts.factory.createPropertyAccessExpression(
1799
+ ts.factory.createIdentifier('z'),
1800
+ ts.factory.createIdentifier('literal'),
1801
+ ),
1786
1802
  undefined,
1787
- [],
1803
+ [literalValue],
1788
1804
  );
1805
+ });
1806
+
1807
+ const unionExpression = ts.factory.createCallExpression(
1808
+ ts.factory.createPropertyAccessExpression(
1809
+ ts.factory.createIdentifier('z'),
1810
+ ts.factory.createIdentifier('union'),
1811
+ ),
1812
+ undefined,
1813
+ [ts.factory.createArrayLiteralExpression(literalSchemas, false)],
1814
+ );
1815
+
1816
+ return required
1817
+ ? unionExpression
1818
+ : ts.factory.createCallExpression(
1819
+ ts.factory.createPropertyAccessExpression(unionExpression, ts.factory.createIdentifier('optional')),
1820
+ undefined,
1821
+ [],
1822
+ );
1823
+ }
1789
1824
  }
1790
1825
 
1791
1826
  switch (prop['type']) {
@@ -185,6 +185,84 @@ describe('TypeScriptCodeGeneratorService', () => {
185
185
  expect(code).toContain('pending');
186
186
  });
187
187
 
188
+ it('should handle numeric enum types with z.union and z.literal', () => {
189
+ const spec: OpenApiSpecType = {
190
+ openapi: '3.0.0',
191
+ info: {
192
+ title: 'Test API',
193
+ version: '1.0.0',
194
+ },
195
+ paths: {},
196
+ components: {
197
+ schemas: {
198
+ Status: {
199
+ type: 'integer',
200
+ enum: [-99, 0, 1, 2],
201
+ },
202
+ ExecutionMode: {
203
+ type: 'integer',
204
+ enum: [1, 2],
205
+ },
206
+ },
207
+ },
208
+ };
209
+
210
+ const code = generator.generate(spec);
211
+ // Numeric enums should use z.union([z.literal(...), ...])
212
+ expect(code).toContain('z.union');
213
+ expect(code).toContain('z.literal');
214
+ expect(code).toContain('-99');
215
+ expect(code).toContain('0');
216
+ expect(code).toContain('1');
217
+ expect(code).toContain('2');
218
+ // Should not use z.enum for numeric enums
219
+ expect(code).not.toContain('Status: z.enum');
220
+ expect(code).not.toContain('ExecutionMode: z.enum');
221
+ });
222
+
223
+ it('should merge baseOptions with request-specific options in #makeRequest', () => {
224
+ const spec: OpenApiSpecType = {
225
+ openapi: '3.0.0',
226
+ info: {
227
+ title: 'Test API',
228
+ version: '1.0.0',
229
+ },
230
+ paths: {
231
+ '/test': {
232
+ get: {
233
+ operationId: 'testEndpoint',
234
+ responses: {
235
+ '200': {
236
+ description: 'Success',
237
+ content: {
238
+ 'application/json': {
239
+ schema: {type: 'string'},
240
+ },
241
+ },
242
+ },
243
+ },
244
+ },
245
+ },
246
+ },
247
+ };
248
+
249
+ const code = generator.generate(spec);
250
+
251
+ // Should call getBaseRequestOptions()
252
+ expect(code).toContain('getBaseRequestOptions()');
253
+
254
+ // Should merge headers: baseHeaders + Content-Type + request headers
255
+ expect(code).toContain('Object.assign');
256
+ expect(code).toContain('baseHeaders');
257
+ expect(code).toContain('Content-Type');
258
+
259
+ // Should merge all options: baseOptions + {method, headers, body}
260
+ expect(code).toMatch(/Object\.assign\s*\(\s*\{\s*\}\s*,\s*baseOptions/);
261
+ expect(code).toContain('method');
262
+ expect(code).toContain('headers');
263
+ expect(code).toContain('body');
264
+ });
265
+
188
266
  it('should handle array types', () => {
189
267
  const spec: OpenApiSpecType = {
190
268
  openapi: '3.0.0',