@@ -89,6 +89,8 @@ Re-run the same command whenever your spec changes — the generator overwrites
8989| ` includeMocks ` | no | ` false ` | Emit a ` .mock.ts ` per endpoint plus ` index.mock.ts ` barrels and a ` mocks.manifest.json ` — requires [ ` @constantant/openapi-resource-mocks ` ] ( https://www.npmjs.com/package/@constantant/openapi-resource-mocks ) |
9090| ` includeMswHandlers ` | no | ` false ` | Emit a ` .msw.ts ` per endpoint plus ` index.msw.ts ` barrels — requires [ ` msw ` ] ( https://mswjs.io ) >= 2.0.0 |
9191| ` specId ` | no | derived from ` baseUrlToken ` | Identifier embedded in every generated ` MockResourceMeta ` and in ` mocks.manifest.json ` . Defaults to ` baseUrlToken ` with ` _BASE_URL ` stripped and lowercased (e.g. ` PETSTORE_BASE_URL ` → ` petstore ` ). Must match the value used when importing the spec into the DevTools panel. |
92+ | ` dateType ` | no | ` string ` | ` string ` (default — no change), ` Date ` , or ` Temporal ` . When set to ` Date ` or ` Temporal ` , emits a typed ` XxxRevived ` alias and a ` reviveXxxDates() ` helper per endpoint whose response contains ` format: date-time ` or ` format: date ` fields. |
93+ | ` readonlyResponses ` | no | ` false ` | Wrap all ` XxxResponse ` and ` XxxError ` type aliases in ` Readonly<> ` to prevent accidental mutation of response data. |
9294| ` verbose ` | no | ` false ` | Print a ` + ` /` ~ ` /` - ` summary of created, updated, and deleted files after generation. |
9395
9496---
@@ -104,6 +106,8 @@ Re-run the same command whenever your spec changes — the generator overwrites
104106 index.mock.ts # (--includeMocks) re-exports all tag mock barrels
105107 index.msw.ts # (--includeMswHandlers) re-exports all tag MSW handler arrays
106108 mocks.manifest.json # (--includeMocks) machine-readable endpoint list + specId for the DevTools panel
109+ webhooks/
110+ {webhook-name}.webhook.ts # (OAS 3.1 webhooks) InjectionToken<HttpInterceptorFn> per webhook
107111 {tag}/
108112 index.ts # re-exports all token files in this tag folder
109113 index.mock.ts # (--includeMocks) re-exports all mock files in this tag
@@ -113,7 +117,7 @@ Re-run the same command whenever your spec changes — the generator overwrites
113117 {operation-id}.msw.ts # (--includeMswHandlers) MSW 2.x handler factory + pre-called array
114118```
115119
116- Tags map to subfolders; untagged operations go into ` default/ ` .
120+ Tags map to subfolders; untagged operations go into ` default/ ` . OAS 3.1 ` webhooks ` map to ` webhooks/ ` .
117121
118122---
119123
@@ -303,6 +307,89 @@ are never bundled into a production build.
303307
304308---
305309
310+ ## Date revival (` --dateType ` )
311+
312+ By default the generator uses ` dateType: 'string' ` — date/time fields stay as
313+ ` string ` exactly as ` openapi-typescript ` emits them. Set ` --dateType=Date ` or
314+ ` --dateType=Temporal ` to also generate a ` XxxRevived ` type alias and a
315+ ` reviveXxxDates() ` helper for any endpoint whose response contains
316+ ` format: date-time ` or ` format: date ` fields:
317+
318+ ``` typescript
319+ // orders/get-order.token.ts (generated with --dateType=Date)
320+
321+ // The raw response type still reflects the schema faithfully:
322+ export type GetOrderResponse =
323+ paths [' /orders/{id}' ][' get' ][' responses' ][' 200' ][' content' ][' application/json' ];
324+
325+ // Revived alias: date fields replaced with Date objects
326+ export type GetOrderRevived = Omit <GetOrderResponse , ' createdAt' | ' updatedAt' > & {
327+ createdAt: Date ;
328+ updatedAt: Date ;
329+ };
330+
331+ // Pure reviver function — does not mutate the raw object
332+ export function reviveGetOrderDates(raw : GetOrderResponse ): GetOrderRevived {
333+ const obj = raw as unknown as Record <string , unknown >;
334+ return {
335+ ... obj ,
336+ createdAt: obj [' createdAt' ] != null ? new Date (obj [' createdAt' ] as string ) : obj [' createdAt' ],
337+ updatedAt: obj [' updatedAt' ] != null ? new Date (obj [' updatedAt' ] as string ) : obj [' updatedAt' ],
338+ } as GetOrderRevived ;
339+ }
340+ ```
341+
342+ For array responses (` GetOrderRevived ` becomes ` (Omit<…> & { … })[] ` ), the function
343+ maps over each element. Using ` --dateType=Temporal ` produces ` Temporal.Instant ` for
344+ ` date-time ` fields and ` Temporal.PlainDate ` for ` date ` fields:
345+
346+ ``` typescript
347+ export type GetOrderRevived = Omit <GetOrderResponse , ' createdAt' > & {
348+ createdAt: Temporal .Instant ;
349+ };
350+ ```
351+
352+ Call the reviver after receiving the resource value:
353+
354+ ``` typescript
355+ readonly order = this .getOrder (' 42' );
356+ readonly orderRevived = computed (() => {
357+ const raw = this .order .value ();
358+ return raw ? reviveGetOrderDates (raw ) : undefined ;
359+ });
360+ ```
361+
362+ ---
363+
364+ ## Readonly responses (` --readonlyResponses ` )
365+
366+ Pass ` --readonlyResponses ` to wrap every emitted ` XxxResponse ` and ` XxxError ` type
367+ alias in ` Readonly<> ` . This prevents accidental mutation of the returned data at the
368+ TypeScript type level:
369+
370+ ``` typescript
371+ // Without --readonlyResponses (default):
372+ export type GetOrderResponse =
373+ paths [' /orders/{id}' ][' get' ][' responses' ][' 200' ][' content' ][' application/json' ];
374+
375+ // With --readonlyResponses:
376+ export type GetOrderResponse =
377+ Readonly <paths [' /orders/{id}' ][' get' ][' responses' ][' 200' ][' content' ][' application/json' ]>;
378+ ```
379+
380+ Multi-status union aliases use ` Readonly<> ` on each branch:
381+
382+ ``` typescript
383+ export type UpsertOrderResponse =
384+ | Readonly <paths [' /orders' ][' put' ][' responses' ][' 200' ][' content' ][' application/json' ]>
385+ | Readonly <paths [' /orders' ][' put' ][' responses' ][' 201' ][' content' ][' application/json' ]>;
386+ ```
387+
388+ ` --readonlyResponses ` works in combination with ` --dateType ` — the ` XxxRevived `
389+ alias also wraps the ` Omit & { … } ` shape in ` Readonly<> ` .
390+
391+ ---
392+
306393## Generated token anatomy
307394
308395### GET with query params
@@ -530,6 +617,124 @@ export type UpsertResourceResponse =
530617The ` httpResource<UpsertResourceResponse> ` call site receives a value that is the
531618union of all possible success shapes.
532619
620+ ### Typed error aliases
621+
622+ For 4xx/5xx responses that carry JSON bodies, the generator emits an ` XxxError ` type alias
623+ alongside the response type. This lets callers type the ` .error() ` signal from ` httpResource ` :
624+
625+ ``` typescript
626+ // Single error code
627+ export type GetOrderError =
628+ paths [' /orders/{id}' ][' get' ][' responses' ][' 404' ][' content' ][' application/json' ];
629+
630+ // Multiple error codes — union
631+ export type GetOrderError =
632+ | paths [' /orders/{id}' ][' get' ][' responses' ][' 400' ][' content' ][' application/json' ]
633+ | paths [' /orders/{id}' ][' get' ][' responses' ][' 404' ][' content' ][' application/json' ];
634+ ```
635+
636+ With ` --readonlyResponses ` each branch is wrapped in ` Readonly<> ` .
637+
638+ ### Enum label/description maps
639+
640+ When query or path params use the vendor extensions ` x-enum-varnames ` and/or
641+ ` x-enum-descriptions ` , the generator emits typed const objects alongside the
642+ ` Params ` type alias. These are useful for building select options and accessible
643+ tooltips without hand-writing display strings:
644+
645+ ``` typescript
646+ // find-pets-by-status.token.ts
647+ // Spec has: enum: [available, pending, sold], x-enum-varnames: [Available, Pending, Sold]
648+ export const findPetsByStatusStatusLabels = {
649+ ' available' : ' Available' ,
650+ ' pending' : ' Pending' ,
651+ ' sold' : ' Sold' ,
652+ } as const ;
653+
654+ // x-enum-descriptions: [Pets ready to adopt, Pending adoption, Already adopted]
655+ export const findPetsByStatusStatusDescriptions = {
656+ ' available' : ' Pets ready to adopt' ,
657+ ' pending' : ' Pending adoption' ,
658+ ' sold' : ' Already adopted' ,
659+ } as const ;
660+ ```
661+
662+ The object key is the raw enum value (as it appears in the API request); the object
663+ value is the human-readable label or description from the extension.
664+
665+ ### Non-default query param serialization
666+
667+ When a query parameter declares a non-default ` style ` in the spec (` deepObject ` ,
668+ ` pipeDelimited ` , or ` spaceDelimited ` ), the generator emits a module-private
669+ ` _serializeParams() ` helper inside the token file. This helper converts the typed
670+ ` XxxParams ` object to a flat ` Record<string, string | readonly string[]> ` before
671+ passing it to ` HttpClient ` :
672+
673+ ``` typescript
674+ // deep-search.token.ts (generated — deepObject param 'filter')
675+ function _serializeParams(p : DeepSearchParams | undefined ): Record <string , string | readonly string []> | undefined {
676+ if (p == null ) return undefined ;
677+ const _out: Record <string , string | readonly string []> = {};
678+ for (const [_k, _v] of Object .entries (p as Record <string , unknown >)) {
679+ if (_v == null ) continue ;
680+ switch (_k ) {
681+ case ' filter' :
682+ for (const [_dk, _dv] of Object .entries (_v as Record <string , unknown >))
683+ if (_dv != null ) _out [' filter[' + _dk + ' ]' ] = String (_dv );
684+ break ;
685+ default :
686+ _out [_k ] = Array .isArray (_v ) ? (_v as unknown []).map (String ) : String (_v as string | number | boolean );
687+ }
688+ }
689+ return _out ;
690+ }
691+ ```
692+
693+ | Spec ` style ` | Serialization |
694+ | ---| ---|
695+ | ` deepObject ` | ` filter[key]=value ` — one query param per nested key |
696+ | ` pipeDelimited ` | ` tags=a\|b\|c ` — pipe-joined array |
697+ | ` spaceDelimited ` | ` tags=a b c ` — space-joined array |
698+
699+ Standard comma-separated arrays use the default ` HttpClient ` serialization; no helper
700+ is emitted for those.
701+
702+ ### Discriminated unions
703+
704+ When a response schema uses ` oneOf ` /` anyOf ` with a discriminator, the generator emits
705+ narrowing helpers alongside the ` XxxResponse ` type:
706+
707+ ``` typescript
708+ // get-animal.token.ts (discriminator: { propertyName: 'type' })
709+ export type GetAnimalDiscriminatorKey = ' dog' | ' cat' ;
710+
711+ // Mapping-style: each variant intersects its component schema with the discriminant literal
712+ export type GetAnimalDog = components [' schemas' ][' Dog' ] & { ' type' : ' dog' };
713+ export type GetAnimalCat = components [' schemas' ][' Cat' ] & { ' type' : ' cat' };
714+
715+ // Convenience union of all narrowed variants
716+ export type GetAnimalDiscriminated = GetAnimalDog | GetAnimalCat ;
717+ ```
718+
719+ When the discriminator resolves from a plain enum (no ` components/schemas ` mapping),
720+ the generator uses ` Extract ` instead:
721+
722+ ``` typescript
723+ export type GetAnimalDog = Extract <GetAnimalResponse , { ' type' : ' dog' }>;
724+ ```
725+
726+ For array responses (` GetAnimalResponse ` is ` Animal[] ` ), ` GetAnimalDiscriminated `
727+ becomes ` (GetAnimalDog | GetAnimalCat)[] ` .
728+
729+ Use the narrowed types at the call site:
730+
731+ ``` typescript
732+ const item = this .getAnimal .value ();
733+ if (item && ' type' in item && item .type === ' dog' ) {
734+ const dog: GetAnimalDog = item ; // narrowed
735+ }
736+ ```
737+
533738### Security schemes
534739
535740The generator emits one file per security scheme. Two patterns are used depending on the
@@ -796,6 +1001,59 @@ navigation in and destroyed on navigation out, with no cross-route state leakage
7961001
7971002---
7981003
1004+ ## OAS 3.1 support
1005+
1006+ The generator handles OpenAPI 3.1 specs in addition to 3.0.x. The main differences
1007+ that affect code generation:
1008+
1009+ ### Type arrays and implicit nullability
1010+
1011+ OAS 3.1 allows ` type: ['string', 'null'] ` instead of ` nullable: true ` . Both forms
1012+ produce the same output from ` openapi-typescript ` (` string | null ` ), so generated
1013+ token types remain correct in either spec version.
1014+
1015+ ### Webhooks
1016+
1017+ OAS 3.1 ` webhooks ` entries are emitted as ` .webhook.ts ` files under a ` webhooks/ `
1018+ subfolder. Each webhook token holds ` InjectionToken<HttpInterceptorFn> ` — the
1019+ consumer registers an interceptor that handles incoming webhook requests:
1020+
1021+ ``` typescript
1022+ // webhooks/order-placed.webhook.ts (generated)
1023+ import { InjectionToken } from ' @angular/core' ;
1024+ import { HttpInterceptorFn } from ' @angular/common/http' ;
1025+ import type { webhooks } from ' ../schema.d' ;
1026+
1027+ export type OrderPlacedWebhookPayload =
1028+ NonNullable <webhooks [' order.placed' ][' post' ][' requestBody' ]>[' content' ][' application/json' ];
1029+
1030+ export type OrderPlacedWebhookResponse =
1031+ webhooks [' order.placed' ][' post' ][' responses' ][' 200' ][' content' ][' application/json' ];
1032+
1033+ export const ORDER_PLACED_WEBHOOK = new InjectionToken <HttpInterceptorFn >(' ORDER_PLACED_WEBHOOK' );
1034+ ```
1035+
1036+ Wire up in ` app.config.ts ` :
1037+
1038+ ``` typescript
1039+ import { ORDER_PLACED_WEBHOOK } from ' @myapp/myapi-data-access' ;
1040+ import type { OrderPlacedWebhookPayload } from ' @myapp/myapi-data-access' ;
1041+
1042+ const myWebhookHandler: HttpInterceptorFn = (req , next ) => {
1043+ // req.body is the incoming webhook payload
1044+ const payload = req .body as OrderPlacedWebhookPayload ;
1045+ // ... handle it
1046+ return next (req );
1047+ };
1048+
1049+ { provide : ORDER_PLACED_WEBHOOK , useValue : myWebhookHandler }
1050+ ```
1051+
1052+ Webhook files are included in the root ` index.ts ` barrel and participate in stale
1053+ file cleanup like all other generated files.
1054+
1055+ ---
1056+
7991057## Implementation notes
8001058
8011059| Step | Tool | Purpose |
0 commit comments