@cerios/openapi-to-zod 1.1.0 → 1.2.0

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/README.md CHANGED
@@ -184,10 +184,11 @@ Examples:
184
184
  | `mode` | `"strict"` \| `"normal"` \| `"loose"` | Validation mode |
185
185
  | `includeDescriptions` | `boolean` | Include JSDoc comments |
186
186
  | `useDescribe` | `boolean` | Add `.describe()` calls |
187
+ | `defaultNullable` | `boolean` | Treat properties as nullable by default when not explicitly specified (default: `false`) |
187
188
  | `schemaType` | `"all"` \| `"request"` \| `"response"` | Schema filtering |
188
189
  | `prefix` | `string` | Prefix for schema names |
189
190
  | `suffix` | `string` | Suffix for schema names |
190
- | `stripSchemaPrefix` | `string \| RegExp` | Strip prefix from schema names before generating (e.g., `"Company.Models."` or `/^[A-Z]+\./`) |
191
+ | `stripSchemaPrefix` | `string` | Strip prefix from schema names before generating using glob patterns (e.g., `"Company.Models."` or `"*.Models."`) |
191
192
  | `showStats` | `boolean` | Include generation statistics |
192
193
  | `request` | `object` | Request-specific options (mode, includeDescriptions, useDescribe) |
193
194
  | `response` | `object` | Response-specific options (mode, includeDescriptions, useDescribe) |
@@ -410,6 +411,68 @@ The generator supports all OpenAPI string formats with Zod v4:
410
411
  | `cidrv4` | `z.cidrv4()` |
411
412
  | `cidrv6` | `z.cidrv6()` |
412
413
 
414
+ ### Custom Date-Time Format
415
+
416
+ By default, the generator uses `z.iso.datetime()` for `date-time` format fields, which requires an ISO 8601 datetime string with a timezone suffix (e.g., `2026-01-07T14:30:00Z`).
417
+
418
+ If your API returns date-times **without the `Z` suffix** (e.g., `2026-01-07T14:30:00`), you can override this with a custom regex pattern:
419
+
420
+ ```typescript
421
+ import { defineConfig } from '@cerios/openapi-to-zod';
422
+
423
+ export default defineConfig({
424
+ defaults: {
425
+ // For date-times without Z suffix
426
+ customDateTimeFormatRegex: '^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$',
427
+ },
428
+ specs: [
429
+ {
430
+ input: 'openapi.yaml',
431
+ output: 'src/schemas.ts',
432
+ },
433
+ ],
434
+ });
435
+ ```
436
+
437
+ **TypeScript Config - RegExp Literals:**
438
+
439
+ In TypeScript config files, you can also use RegExp literals (which don't require double-escaping):
440
+
441
+ ```typescript
442
+ export default defineConfig({
443
+ defaults: {
444
+ // Use RegExp literal (single escaping)
445
+ customDateTimeFormatRegex: /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/,
446
+ },
447
+ specs: [
448
+ {
449
+ input: 'openapi.yaml',
450
+ output: 'src/schemas.ts',
451
+ },
452
+ ],
453
+ });
454
+ ```
455
+
456
+ **Common Custom Formats:**
457
+
458
+ | Use Case | String Pattern (JSON/YAML) | RegExp Literal (TypeScript) |
459
+ |----------|----------------------------|----------------------------|
460
+ | No timezone suffix<br>`2026-01-07T14:30:00` | `'^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$'` | `/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/` |
461
+ | With milliseconds, no Z<br>`2026-01-07T14:30:00.123` | `'^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}$'` | `/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}$/` |
462
+ | Optional Z suffix<br>`2026-01-07T14:30:00` or<br>`2026-01-07T14:30:00Z` | `'^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z?$'` | `/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z?$/` |
463
+ | With milliseconds + optional Z<br>`2026-01-07T14:30:00.123Z` | `'^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z?$'` | `/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z?$/` |
464
+
465
+ **Generated Output:**
466
+
467
+ When using a custom regex, the generator will produce:
468
+
469
+ ```typescript
470
+ // Instead of: z.iso.datetime()
471
+ // You get: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/)
472
+ ```
473
+
474
+ **Note:** This option only affects `date-time` format fields. Other formats (like `date`, `email`, `uuid`) remain unchanged.
475
+
413
476
  ## Advanced Features
414
477
 
415
478
  ### Operation Filtering
@@ -563,6 +626,68 @@ export const userSchema = z.object({
563
626
 
564
627
  OpenAPI's `nullable: true` is converted to `.nullable()`
565
628
 
629
+ #### Default Nullable Behavior
630
+
631
+ By default, properties are only nullable when explicitly marked with `nullable: true` (OpenAPI 3.0) or `type: ["string", "null"]` (OpenAPI 3.1).
632
+
633
+ However, many teams follow the industry de facto standard for OpenAPI 3.0.x where properties are assumed nullable unless explicitly constrained. You can enable this behavior with the `defaultNullable` option:
634
+
635
+ ```typescript
636
+ export default defineConfig({
637
+ specs: [{
638
+ input: 'openapi.yaml',
639
+ output: 'schemas.ts',
640
+ defaultNullable: true, // Treat unspecified properties as nullable
641
+ }]
642
+ });
643
+ ```
644
+
645
+ **Behavior comparison:**
646
+
647
+ | Schema Property | `defaultNullable: false` (default) | `defaultNullable: true` |
648
+ |-----------------|-------------------------------------|-------------------------|
649
+ | `nullable: true` | `.nullable()` | `.nullable()` |
650
+ | `nullable: false` | No `.nullable()` | No `.nullable()` |
651
+ | No `nullable` specified | No `.nullable()` | `.nullable()` |
652
+
653
+ **Example:**
654
+
655
+ ```yaml
656
+ User:
657
+ type: object
658
+ properties:
659
+ id:
660
+ type: integer
661
+ name:
662
+ type: string
663
+ email:
664
+ type: string
665
+ nullable: true
666
+ phone:
667
+ type: string
668
+ nullable: false
669
+ ```
670
+
671
+ **With `defaultNullable: false` (default):**
672
+ ```typescript
673
+ export const userSchema = z.object({
674
+ id: z.number().int(),
675
+ name: z.string(), // Not nullable (no annotation)
676
+ email: z.string().nullable(), // Explicitly nullable
677
+ phone: z.string(), // Explicitly not nullable
678
+ });
679
+ ```
680
+
681
+ **With `defaultNullable: true`:**
682
+ ```typescript
683
+ export const userSchema = z.object({
684
+ id: z.number().int().nullable(), // Nullable by default
685
+ name: z.string().nullable(), // Nullable by default
686
+ email: z.string().nullable(), // Explicitly nullable
687
+ phone: z.string(), // Explicitly NOT nullable (respected)
688
+ });
689
+ ```
690
+
566
691
  ### Schema Composition
567
692
 
568
693
  - `allOf` → `.merge()` for objects, `.and()` for primitives
@@ -571,10 +696,46 @@ OpenAPI's `nullable: true` is converted to `.nullable()`
571
696
 
572
697
  ### Enums
573
698
 
574
- Enums are generated as Zod enums with:
575
- - Proper string value handling
576
- - Zod schema using `z.enum()`
577
- - TypeScript type inference from the Zod schema
699
+ Enums are generated based on their value types:
700
+
701
+ - **String enums**: `z.enum()` for type-safe string unions
702
+ - **Numeric enums**: `z.union([z.literal(n), ...])` for proper number types
703
+ - **Boolean enums**: `z.boolean()` for true/false values
704
+ - **Mixed enums**: `z.union([z.literal(...), ...])` for heterogeneous values
705
+
706
+ **Examples:**
707
+
708
+ ```yaml
709
+ # String enum
710
+ Status:
711
+ type: string
712
+ enum: [active, inactive, pending]
713
+
714
+ # Integer enum
715
+ Priority:
716
+ type: integer
717
+ enum: [0, 1, 2, 3]
718
+
719
+ # Mixed enum
720
+ Value:
721
+ enum: [0, "none", 1, "some"]
722
+ ```
723
+
724
+ **Generated schemas:**
725
+
726
+ ```typescript
727
+ // String enum → z.enum()
728
+ export const statusSchema = z.enum(["active", "inactive", "pending"]);
729
+ export type Status = z.infer<typeof statusSchema>; // "active" | "inactive" | "pending"
730
+
731
+ // Integer enum → z.union with z.literal
732
+ export const prioritySchema = z.union([z.literal(0), z.literal(1), z.literal(2), z.literal(3)]);
733
+ export type Priority = z.infer<typeof prioritySchema>; // 0 | 1 | 2 | 3
734
+
735
+ // Mixed enum → z.union with z.literal
736
+ export const valueSchema = z.union([z.literal(0), z.literal("none"), z.literal(1), z.literal("some")]);
737
+ export type Value = z.infer<typeof valueSchema>; // 0 | "none" | 1 | "some"
738
+ ```
578
739
 
579
740
  ## Schema Naming
580
741
 
@@ -684,39 +845,39 @@ export default defineConfig({
684
845
  });
685
846
  ```
686
847
 
687
- #### Regex Patterns
848
+ #### Glob Patterns
688
849
 
689
- Use regex patterns to strip dynamic prefixes:
850
+ Use glob patterns to strip dynamic prefixes:
690
851
 
691
852
  ```typescript
692
853
  export default defineConfig({
693
854
  specs: [{
694
855
  input: 'openapi.yaml',
695
856
  output: 'schemas.ts',
696
- // Strip any namespace prefix ending with a dot
697
- stripSchemaPrefix: '^[A-Z][a-z]+\\.'
857
+ // Strip any namespace prefix with wildcard
858
+ stripSchemaPrefix: '*.Models.'
698
859
  }]
699
860
  });
700
861
  ```
701
862
 
702
- **Regex Auto-Detection:**
863
+ **Glob Pattern Syntax:**
703
864
 
704
- Regex patterns are auto-detected if they contain: `^`, `$`, `\\d`, `\\w`, `\\s`, `.*`, `.+`, `[]`, `()`
865
+ Glob patterns support powerful matching using [minimatch](https://github.com/isaacs/minimatch):
866
+ - `*` matches any characters within a single segment (stops at `.`)
867
+ - `**` matches any characters across multiple segments (crosses `.` boundaries)
868
+ - `?` matches a single character
869
+ - `[abc]` matches any character in the set
870
+ - `{a,b}` matches any of the alternatives
871
+ - `!(pattern)` matches anything except the pattern
705
872
 
706
873
  ```typescript
707
- // These are all treated as regex patterns:
708
- stripSchemaPrefix: '^Company\\.' // Starts with ^
709
- stripSchemaPrefix: '[A-Z]+\\.' // Contains []
710
- stripSchemaPrefix: '.*\\.Models\\.' // Contains .*
711
-
712
- // This is a literal string:
713
- stripSchemaPrefix: 'Company.Models.' // No regex markers
714
- ```
715
-
716
- For TypeScript configs, you can also use `RegExp` objects:
717
-
718
- ```typescript
719
- stripSchemaPrefix: /^[A-Z][a-z]+\./
874
+ // Examples of glob patterns:
875
+ stripSchemaPrefix: '*.Models.' // Matches Company.Models., App.Models.
876
+ stripSchemaPrefix: '**.Models.' // Matches any depth: Company.Api.Models., App.V2.Models.
877
+ stripSchemaPrefix: 'Company.{Models,Services}.' // Matches Company.Models. or Company.Services.
878
+ stripSchemaPrefix: 'api_v[0-9]_' // Matches api_v1_, api_v2_, etc.
879
+ stripSchemaPrefix: 'v*.*.' // Matches v1.0., v2.1., etc.
880
+ stripSchemaPrefix: '!(Internal)*.' // Matches any prefix except those starting with Internal
720
881
  ```
721
882
 
722
883
  #### Common Patterns
@@ -730,24 +891,44 @@ stripSchemaPrefix: /^[A-Z][a-z]+\./
730
891
  // Company.Models.Post → Post
731
892
  ```
732
893
 
733
- **Pattern 2: Multiple Namespaces**
894
+ **Pattern 2: Multiple Namespaces with Wildcard**
734
895
  ```typescript
735
896
  {
736
- stripSchemaPrefix: '^[A-Za-z]+\\.Models\\.'
897
+ stripSchemaPrefix: '*.Models.'
737
898
  }
738
899
  // MyApp.Models.User → User
739
900
  // OtherApp.Models.User → User
901
+ // Company.Models.Post → Post
740
902
  ```
741
903
 
742
- **Pattern 3: Version Prefixes**
904
+ **Pattern 3: Multiple Namespace Types**
743
905
  ```typescript
744
906
  {
745
- stripSchemaPrefix: '^v\\d+\\.'
907
+ stripSchemaPrefix: '*.{Models,Services}.'
908
+ }
909
+ // App.Models.User → User
910
+ // App.Services.UserService → UserService
911
+ ```
912
+
913
+ **Pattern 4: Version Prefixes with Character Class**
914
+ ```typescript
915
+ {
916
+ stripSchemaPrefix: 'v[0-9].'
746
917
  }
747
918
  // v1.User → User
748
919
  // v2.Product → Product
749
920
  ```
750
921
 
922
+ **Pattern 5: Versioned Prefixes with Wildcards**
923
+ ```typescript
924
+ {
925
+ stripSchemaPrefix: 'api_v*_'
926
+ }
927
+ // api_v1_User → User
928
+ // api_v2_Product → Product
929
+ // api_v10_Comment → Comment
930
+ ```
931
+
751
932
  #### Interaction with prefix/suffix Options
752
933
 
753
934
  `stripSchemaPrefix` is applied **before** `prefix` and `suffix` options: