Feature add typescript express zod server and client#23132
Feature add typescript express zod server and client#23132istvanfedak wants to merge 15 commits into
Conversation
There was a problem hiding this comment.
23 issues found across 31 files
Since this is your first cubic review, here's how it works:
- cubic automatically reviews your code and comments on bugs and improvements
- Teach cubic by replying to its comments. cubic learns from your replies and gets better over time
- Add one-off context when rerunning by tagging
@cubic-dev-aiwith guidance or docs links (includingllms.txt) - Ask questions if you need clarification on any suggestion
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| @@ -0,0 +1,6 @@ | |||
| generatorName: typescript-express-zod | |||
| outputDir: samples/server/petstore/typescript/express/zod | |||
There was a problem hiding this comment.
can you please setup a new GitHub workflow similar to https://github.com/OpenAPITools/openapi-generator/blob/master/.github/workflows/samples-typescript-nestjs-server.yaml for this output folder so that future changes will be tested by the CI automatically?
- Updated Zod to v4 - Update enum handling in Zod schemas to support allowable values. - Make discriminator properties required in Zod schemas for better type inference. - Refactor response headers in handlers for improved SSE support. - Change merge to extend for intersection parents in schemas. - Ensure dataType is correctly formatted in generated types.
9f7c6fc to
9625d8d
Compare
…generators Fixes for correctness/robustness issues raised in review of the typescript-express-zod-server and typescript-express-zod-client generators: - DTO mappers: guard referenced properties with `== null` (preserve null, never pass null into a mapper) for arrays and single refs, both directions. - Discriminated-union mappers: add a default case that throws instead of falling through to an undefined return. - Nullable handling: emit `T | null` in domain/DTO types and `.nullable()` in Zod schemas when a property is nullable. - Path parameters: use the schema-derived dataType instead of hardcoded string. - Request body: honor optionality (`?`) and use the correct (unescaped) type. - Models without properties: emit fallback Zod schema and DTO type alias. - Server handlers: validate the mapped wire payload (responseDto) against the response schema rather than the pre-mapped domain object. - SSE: client parser handles CRLF framing and multi-line data; server detects promise-wrapped async iterables. - Client: stop parsing JSON on void/no-content (e.g. 204) responses. - Swagger UI: escape `<` in the embedded schema to prevent script breakout. - Error type guards: guard against null/non-object before reading `.code`. - Router types: dedicated HandlerOverrides type without the Swagger-UI scopes. - Implement the previously failing server model unit test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
2 issues found across 16 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
…andling Two follow-up fixes from the automated review of the previous commit: - Discriminated-union mappers: `mapFrom*Dto` (wire -> domain, request path) now throws a ZodError on an unknown discriminator so the handler maps it to a 4xx client error instead of a 500. `mapTo*Dto` (domain -> wire, server output) keeps throwing a plain Error (5xx), which is correct for a server-produced value. - Server handlers validate the response payload BEFORE sending it; a schema mismatch is a server contract violation, so it now fails with a 5xx via next(unhandledException) without shipping invalid data to the client (previously the post-send validation could not prevent the bad payload from being sent). The ZodError import in the DTO mappers is emitted only when a discriminated union is present (new hasDiscriminatedUnion template flag), avoiding an unused import under `noUnusedLocals`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
3 issues found across 5 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
…edback Follow-up fixes from the automated review of the previous commit: - Client mappers: an unknown discriminator in mapFrom*Dto processes a server response, not client input, so it now throws a plain Error (unexpected server contract drift) instead of a ZodError. The client no longer imports ZodError (and the client codegen drops the now-unused hasDiscriminatedUnion flag). The server mapFrom*Dto keeps throwing ZodError, since there it maps client input and should surface as a 4xx. - Server handlers: extract the duplicated response map/validate/send logic (SSE non-streaming and non-SSE branches) into a single sendResponse() helper, removing the risk of the two copies drifting apart. - Response validation failures now go through errors.validationErrors(500, issues) instead of unhandledException, preserving the structured ZodIssue[] details and staying consistent with request validation, while keeping the 5xx status for a server-side contract violation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
1 issue found across 3 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
…rror path The client's mapFrom*Dto maps server responses. Throwing a generic Error for an unknown discriminator bypassed the http-client's `instanceof ZodError` branch (mapZodError), dropping structured issue details and any user-provided mapZodError override, and funnelling the failure into mapUnhandledException. Restore throwing a ZodError so the failure flows through the client's existing response-validation error channel, but frame it correctly as unexpected server-response contract drift (not a client input error). The ZodError import is re-gated on the hasDiscriminatedUnion flag so specs without discriminated unions don't get an unused import under noUnusedLocals. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ent generators Adds a GitHub Actions workflow (modeled on samples-typescript-nestjs-server) that runs the generated express-zod output through a real runtime test suite on every PR, so template/codegen regressions are caught automatically. - bin/configs: replace the broken typescript-express-zod-petstore-new.yaml (invalid generatorName / nonexistent templateDir) with proper server and client configs. - Add a small input spec (typescript-express-zod.yaml) that stays within the generators' currently supported surface (single-model request bodies and responses) while exercising path/query params, nested & nullable $ref properties, an enum ref, a void operation, and a discriminated union. - samples/server/.../typescript-express-zod-server: hand-written service implementations + Express app wired via getRouter(), and a supertest e2e suite. npm test = tsc --noEmit (all generated files, strict + noUnusedLocals) + jest. - samples/client/.../typescript-express-zod-client: a jest suite that drives the generated client with an injected fetch, covering parse -> Zod validation -> DTO mapping, the error path, a custom mapZodError override, and a discriminated union. - .github/workflows/samples-typescript-express-zod.yaml: matrix of both samples x node 20/22, npm i + npm run test. Also fixes the client http-client template: the void/no-content branch left the fetch response unused, which fails under noUnusedLocals (surfaced by the sample's void DELETE operation). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
6 issues found across 45 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="samples/server/petstore/typescript-express-zod-server/builds/default/express/router-factory.ts">
<violation number="1" location="samples/server/petstore/typescript-express-zod-server/builds/default/express/router-factory.ts:138">
P2: The Swagger UI `requestInterceptor` builds request URLs with two fragile patterns: `new URL(request.url)` will throw if Swagger emits a relative request URL (common when the OpenAPI spec uses relative server URLs), because the `URL` constructor requires a `base` parameter for relative inputs. Additionally, `currentUrl.href` preserves any hash fragment from the current page (e.g. `#/operationName`), so string-concatenating the API path onto it places the path after the hash, producing an invalid URL. Both cases break the 'Try it out' requests in the generated docs. The interceptor should use `new URL(request.url, window.location.href)` and construct the final URL via the `URL` constructor rather than string concatenation.</violation>
</file>
<file name="samples/client/petstore/typescript-express-zod-client/tsconfig.json">
<violation number="1" location="samples/client/petstore/typescript-express-zod-client/tsconfig.json:7">
P2: This `tsconfig.json` mixes production and test compilation in a single configuration file. Including both `builds/**/*.ts` and `test/**/*.ts` alongside `types: ["node", "jest"]` makes Jest globals globally available during type-checking of production code. This can mask accidental use of `describe`, `it`, or `expect` in generated client source files. Additionally, `outDir` is set without `noEmit`, so the configuration structurally targets emission of both source and test files into `dist`. Existing samples in this repository (e.g., `typescript-axios`) already use separate tsconfigs for builds and tests; consider splitting this into a root build tsconfig and a test-specific tsconfig, or at least moving `jest` to a test-only tsconfig and adding `noEmit: true` here.</violation>
</file>
<file name="samples/server/petstore/typescript-express-zod-server/builds/default/schemas.ts">
<violation number="1" location="samples/server/petstore/typescript-express-zod-server/builds/default/schemas.ts:43">
P2: Using `z.coerce.number().int()` for request-body integer fields can silently corrupt invalid payloads: `null` and empty strings are coerced to `0` by JavaScript `Number()`. For example, sending `{"authorId": null}` to the create-book endpoint yields `authorId: 0` instead of a validation error, because the field is optional but not nullable. Path and query params legitimately need coercion from strings, but body fields should reject `null` when the schema is not marked nullable.</violation>
<violation number="2" location="samples/server/petstore/typescript-express-zod-server/builds/default/schemas.ts:69">
P1: Boolean query parameters received from Express are strings (e.g. `"true"`), but the generated schema uses `z.boolean().optional()` without coercion. A request like `GET /books/1?expand=true` will fail schema validation because `"true"` is not a boolean primitive. Numeric params use `z.coerce.number()` for string values; boolean params should follow the same pattern or use an equivalent preprocessing/coercion strategy.</violation>
</file>
<file name="samples/client/petstore/typescript-express-zod-client/builds/default/http-client.ts">
<violation number="1" location="samples/client/petstore/typescript-express-zod-client/builds/default/http-client.ts:134">
P1: `deleteBook` silently treats HTTP error responses as success because it never checks `response.status`. Unlike the other methods, it does not call `response.json()` (which would at least throw on an unexpected body), so a 404 or 500 from the server resolves to a successful `Promise<void>`. The caller may assume a resource was deleted when it was not.</violation>
</file>
<file name="samples/server/petstore/typescript-express-zod-server/builds/default/express/handlers.ts">
<violation number="1" location="samples/server/petstore/typescript-express-zod-server/builds/default/express/handlers.ts:292">
P1: The `getHttpStatus` helper defaults to the success code (200) whenever an error item lacks a valid `status` field, but the `ModelError` type does not actually include `status` at all. This means every handled service error will be emitted with HTTP 200, causing clients to treat failures as successes. Consider adding `status` to `ModelError` (and its schema/DTO) so the reducer can compute a meaningful status, or at least default to a 4xx/5xx code when `status` is missing instead of the success code.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
|
|
||
| export const GetBookParamsSchema = z.object({ | ||
| bookId: z.coerce.number().int(), | ||
| expand: z.boolean().optional(), |
There was a problem hiding this comment.
P1: Boolean query parameters received from Express are strings (e.g. "true"), but the generated schema uses z.boolean().optional() without coercion. A request like GET /books/1?expand=true will fail schema validation because "true" is not a boolean primitive. Numeric params use z.coerce.number() for string values; boolean params should follow the same pattern or use an equivalent preprocessing/coercion strategy.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/server/petstore/typescript-express-zod-server/builds/default/schemas.ts, line 69:
<comment>Boolean query parameters received from Express are strings (e.g. `"true"`), but the generated schema uses `z.boolean().optional()` without coercion. A request like `GET /books/1?expand=true` will fail schema validation because `"true"` is not a boolean primitive. Numeric params use `z.coerce.number()` for string values; boolean params should follow the same pattern or use an equivalent preprocessing/coercion strategy.</comment>
<file context>
@@ -0,0 +1,75 @@
+
+export const GetBookParamsSchema = z.object({
+ bookId: z.coerce.number().int(),
+ expand: z.boolean().optional(),
+});
+
</file context>
| try { | ||
| const response = await this.fetch<unknown>(url, { method: "DELETE" }); | ||
| // No response body expected (e.g. 204 No Content); nothing to parse. | ||
| void response; |
There was a problem hiding this comment.
P1: deleteBook silently treats HTTP error responses as success because it never checks response.status. Unlike the other methods, it does not call response.json() (which would at least throw on an unexpected body), so a 404 or 500 from the server resolves to a successful Promise<void>. The caller may assume a resource was deleted when it was not.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/petstore/typescript-express-zod-client/builds/default/http-client.ts, line 134:
<comment>`deleteBook` silently treats HTTP error responses as success because it never checks `response.status`. Unlike the other methods, it does not call `response.json()` (which would at least throw on an unexpected body), so a 404 or 500 from the server resolves to a successful `Promise<void>`. The caller may assume a resource was deleted when it was not.</comment>
<file context>
@@ -0,0 +1,178 @@
+ try {
+ const response = await this.fetch<unknown>(url, { method: "DELETE" });
+ // No response body expected (e.g. 204 No Content); nothing to parse.
+ void response;
+ return;
+ } catch (err) {
</file context>
| (Array.isArray(candidate.data) && candidate.data.length === 0); | ||
| } | ||
|
|
||
| function getHttpStatus(success: number, result: unknown): number { |
There was a problem hiding this comment.
P1: The getHttpStatus helper defaults to the success code (200) whenever an error item lacks a valid status field, but the ModelError type does not actually include status at all. This means every handled service error will be emitted with HTTP 200, causing clients to treat failures as successes. Consider adding status to ModelError (and its schema/DTO) so the reducer can compute a meaningful status, or at least default to a 4xx/5xx code when status is missing instead of the success code.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/server/petstore/typescript-express-zod-server/builds/default/express/handlers.ts, line 292:
<comment>The `getHttpStatus` helper defaults to the success code (200) whenever an error item lacks a valid `status` field, but the `ModelError` type does not actually include `status` at all. This means every handled service error will be emitted with HTTP 200, causing clients to treat failures as successes. Consider adding `status` to `ModelError` (and its schema/DTO) so the reducer can compute a meaningful status, or at least default to a 4xx/5xx code when `status` is missing instead of the success code.</comment>
<file context>
@@ -0,0 +1,300 @@
+ (Array.isArray(candidate.data) && candidate.data.length === 0);
+}
+
+function getHttpStatus(success: number, result: unknown): number {
+ if (!isError(result)) return success;
+ return result.errors.reduce((max, item) => {
</file context>
| requestInterceptor: (request) => { | ||
| const currentUrl = new URL(window.location.href); | ||
| const requestUrl = new URL(request.url); | ||
| const newUrl = currentUrl.href + requestUrl.href.substring(requestUrl.origin.length) |
There was a problem hiding this comment.
P2: The Swagger UI requestInterceptor builds request URLs with two fragile patterns: new URL(request.url) will throw if Swagger emits a relative request URL (common when the OpenAPI spec uses relative server URLs), because the URL constructor requires a base parameter for relative inputs. Additionally, currentUrl.href preserves any hash fragment from the current page (e.g. #/operationName), so string-concatenating the API path onto it places the path after the hash, producing an invalid URL. Both cases break the 'Try it out' requests in the generated docs. The interceptor should use new URL(request.url, window.location.href) and construct the final URL via the URL constructor rather than string concatenation.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/server/petstore/typescript-express-zod-server/builds/default/express/router-factory.ts, line 138:
<comment>The Swagger UI `requestInterceptor` builds request URLs with two fragile patterns: `new URL(request.url)` will throw if Swagger emits a relative request URL (common when the OpenAPI spec uses relative server URLs), because the `URL` constructor requires a `base` parameter for relative inputs. Additionally, `currentUrl.href` preserves any hash fragment from the current page (e.g. `#/operationName`), so string-concatenating the API path onto it places the path after the hash, producing an invalid URL. Both cases break the 'Try it out' requests in the generated docs. The interceptor should use `new URL(request.url, window.location.href)` and construct the final URL via the `URL` constructor rather than string concatenation.</comment>
<file context>
@@ -0,0 +1,199 @@
+ requestInterceptor: (request) => {
+ const currentUrl = new URL(window.location.href);
+ const requestUrl = new URL(request.url);
+ const newUrl = currentUrl.href + requestUrl.href.substring(requestUrl.origin.length)
+ request.url = newUrl;
+ return request;
</file context>
| "module": "commonjs", | ||
| "moduleResolution": "node", | ||
| "lib": ["ES2022"], | ||
| "types": ["node", "jest"], |
There was a problem hiding this comment.
P2: This tsconfig.json mixes production and test compilation in a single configuration file. Including both builds/**/*.ts and test/**/*.ts alongside types: ["node", "jest"] makes Jest globals globally available during type-checking of production code. This can mask accidental use of describe, it, or expect in generated client source files. Additionally, outDir is set without noEmit, so the configuration structurally targets emission of both source and test files into dist. Existing samples in this repository (e.g., typescript-axios) already use separate tsconfigs for builds and tests; consider splitting this into a root build tsconfig and a test-specific tsconfig, or at least moving jest to a test-only tsconfig and adding noEmit: true here.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/petstore/typescript-express-zod-client/tsconfig.json, line 7:
<comment>This `tsconfig.json` mixes production and test compilation in a single configuration file. Including both `builds/**/*.ts` and `test/**/*.ts` alongside `types: ["node", "jest"]` makes Jest globals globally available during type-checking of production code. This can mask accidental use of `describe`, `it`, or `expect` in generated client source files. Additionally, `outDir` is set without `noEmit`, so the configuration structurally targets emission of both source and test files into `dist`. Existing samples in this repository (e.g., `typescript-axios`) already use separate tsconfigs for builds and tests; consider splitting this into a root build tsconfig and a test-specific tsconfig, or at least moving `jest` to a test-only tsconfig and adding `noEmit: true` here.</comment>
<file context>
@@ -0,0 +1,20 @@
+ "module": "commonjs",
+ "moduleResolution": "node",
+ "lib": ["ES2022"],
+ "types": ["node", "jest"],
+ "strict": true,
+ "noUnusedLocals": true,
</file context>
| export const CreateBookRequestSchema = z.object({ | ||
| title: z.string(), | ||
| genre: GenreSchema.optional(), | ||
| authorId: z.coerce.number().int().optional(), |
There was a problem hiding this comment.
P2: Using z.coerce.number().int() for request-body integer fields can silently corrupt invalid payloads: null and empty strings are coerced to 0 by JavaScript Number(). For example, sending {"authorId": null} to the create-book endpoint yields authorId: 0 instead of a validation error, because the field is optional but not nullable. Path and query params legitimately need coercion from strings, but body fields should reject null when the schema is not marked nullable.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/server/petstore/typescript-express-zod-server/builds/default/schemas.ts, line 43:
<comment>Using `z.coerce.number().int()` for request-body integer fields can silently corrupt invalid payloads: `null` and empty strings are coerced to `0` by JavaScript `Number()`. For example, sending `{"authorId": null}` to the create-book endpoint yields `authorId: 0` instead of a validation error, because the field is optional but not nullable. Path and query params legitimately need coercion from strings, but body fields should reject `null` when the schema is not marked nullable.</comment>
<file context>
@@ -0,0 +1,75 @@
+export const CreateBookRequestSchema = z.object({
+ title: z.string(),
+ genre: GenreSchema.optional(),
+ authorId: z.coerce.number().int().optional(),
+});
+
</file context>
| authorId: z.coerce.number().int().optional(), | |
| authorId: z.number().int().optional(), |
…uards, and SSE type rendering
Addresses codegen discrepancies across the TypeScript-Express-Zod generator, server route templates, and client SDK output (Findings 1-4, 6-8, 10).
TypescriptExpressZodSupport.java:
- Fix Enum-$ref & free-form classification: Ensure leaves check `isEnum || isEnumRef`. Fall back to identity for free-form objects instead of generating fabricated Dto types/mappers (e.g., `dtos.StatusDto`).
- Implement recursive shape resolution: Introduce depth-suffixed lambdas for `shapeOf`, `arrayShape`, and `mapShape`. Nested arrays (e.g., `Array<Array<Item>>`) now structurally align across DTO arrays, Zod schemas, and runtime mappers.
- Support nullable items: Properly emit `.nullable()` schemas alongside null-skipping element mappers.
- Refactor `resolve()` signature to eliminate the unreachable 9-parameter variant, degrading to identity instead of fabricating an arbitrary DTO name when `rp == null`.
Server Templates:
- Guard body `mapFn` execution: Wrap body mappers in type-guards (e.g., checking `Array.isArray(b)`) to ensure malformed or absent bodies correctly fall through to Zod validation (400) instead of throwing runtime TypeErrors (500).
- Fix SSE type casting: Render explicit `Promise<{{{responseDomainGeneric}}}>` wrappers for SSE branches returning JSON array 200 responses, cleaning up unused generic bindings.
- Update `buildZodType` body fallback to emit standard Zod primitives (`z.string()`, `z.number()`, `z.boolean()`) for dereferenced-primitive bodies.
Client SDK:
- Handle optional bodies cleanly: Apply an undefined-check wrapper on optional body params to preserve fallback behavior for absent bodies instead of causing TS2345 compiler errors or runtime crashes.
- Fix SSE client mapping: Ensure `*Stream` methods structurally map the outbound payload (e.g., `createRecipeStream` properly invokes its matching DTO mapper).
- Align `buildZodType` primitive fallback behavior with the server implementation.
Verification:
- Added comprehensive integration test spec covering nested arrays, enum-refs, free-form objects, nullable items, and optional bodies.
- All 6 sandbox outputs pass validation (`npm test` exit 0, `tsc` compilation success).
Deferred: Non-model SSE event naming conventions, ResponseSchema collisions, query key escaping, and `buildZodType` duplication.
There was a problem hiding this comment.
3 issues found across 9 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="modules/openapi-generator/src/main/resources/typescript-express-zod-server/schemas.mustache">
<violation number="1" location="modules/openapi-generator/src/main/resources/typescript-express-zod-server/schemas.mustache:82">
P1: Model schema exports and per-operation response schema exports share the same `*Schema` naming convention in `schemas.ts`, which can produce duplicate export names when a model class happens to match an operation response schema name (e.g., model `LoginResponse` and operationId `login` both emit `LoginResponseSchema`). Consider adding a reserved prefix or collision detection to keep the two namespaces distinct.</violation>
</file>
<file name="modules/openapi-generator/src/main/resources/typescript-express-zod-client/schemas.mustache">
<violation number="1" location="modules/openapi-generator/src/main/resources/typescript-express-zod-client/schemas.mustache:66">
P1: Operation response schema names can collide with model schema exports in the same `schemas.ts` module. For example, a model named `LoginResponse` produces `export const LoginResponseSchema = ...`, and an operation with `operationId: "login"` produces the same export name. This causes a TypeScript redeclaration error at build time because there is no deduplication logic in the generator. Consider renaming operation response schemas to a pattern that cannot overlap with model schema names, such as prefixing with the operation ID explicitly or checking for collisions during code generation.</violation>
</file>
<file name="modules/openapi-generator/src/main/resources/typescript-express-zod-client/http-client.mustache">
<violation number="1" location="modules/openapi-generator/src/main/resources/typescript-express-zod-client/http-client.mustache:202">
P2: The SSE return type is constructed as `types.{{{sseType}}}` where `sseType` is set from `sseMt.getSchema().getComplexType()` or `getDataType()`. For simple model refs this yields `types.Pet`, which is fine. However, `getDataType()` can return composite TypeScript expressions such as `Array<Pet>`, `Pet[]`, or union types like `Pet | Cat`. Prepending `types.` to these produces invalid TypeScript (e.g., `types.Array<Pet>` or `types.Pet | Cat`). In contrast, the non-SSE response path uses `responseDomainGeneric`, which is recursively built by `TypescriptExpressZodSupport.resolveResponse()` and correctly qualifies nested types. The SSE path bypasses this robust resolution, creating a real bug for streaming endpoints with array, union, or generic event types.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| {{/allOperations}} | ||
| {{#allOperations}} | ||
| {{#responseType}} | ||
| export const {{operationIdPascal}}ResponseSchema = {{{responseZodExpr}}}; |
There was a problem hiding this comment.
P1: Model schema exports and per-operation response schema exports share the same *Schema naming convention in schemas.ts, which can produce duplicate export names when a model class happens to match an operation response schema name (e.g., model LoginResponse and operationId login both emit LoginResponseSchema). Consider adding a reserved prefix or collision detection to keep the two namespaces distinct.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/resources/typescript-express-zod-server/schemas.mustache, line 82:
<comment>Model schema exports and per-operation response schema exports share the same `*Schema` naming convention in `schemas.ts`, which can produce duplicate export names when a model class happens to match an operation response schema name (e.g., model `LoginResponse` and operationId `login` both emit `LoginResponseSchema`). Consider adding a reserved prefix or collision detection to keep the two namespaces distinct.</comment>
<file context>
@@ -71,9 +71,15 @@ export const {{operationIdPascal}}ParamsSchema = z.object({
{{/allOperations}}
+{{#allOperations}}
+{{#responseType}}
+export const {{operationIdPascal}}ResponseSchema = {{{responseZodExpr}}};
+
+{{/responseType}}
</file context>
| {{/models}} | ||
| {{#allOperations}} | ||
| {{#responseType}} | ||
| export const {{operationIdPascal}}ResponseSchema = {{{responseZodExpr}}}; |
There was a problem hiding this comment.
P1: Operation response schema names can collide with model schema exports in the same schemas.ts module. For example, a model named LoginResponse produces export const LoginResponseSchema = ..., and an operation with operationId: "login" produces the same export name. This causes a TypeScript redeclaration error at build time because there is no deduplication logic in the generator. Consider renaming operation response schemas to a pattern that cannot overlap with model schema names, such as prefixing with the operation ID explicitly or checking for collisions during code generation.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/resources/typescript-express-zod-client/schemas.mustache, line 66:
<comment>Operation response schema names can collide with model schema exports in the same `schemas.ts` module. For example, a model named `LoginResponse` produces `export const LoginResponseSchema = ...`, and an operation with `operationId: "login"` produces the same export name. This causes a TypeScript redeclaration error at build time because there is no deduplication logic in the generator. Consider renaming operation response schemas to a pattern that cannot overlap with model schema names, such as prefixing with the operation ID explicitly or checking for collisions during code generation.</comment>
<file context>
@@ -61,3 +61,9 @@ export const {{classname}}Schema = z.object({}).catchall(z.unknown());
{{/models}}
+{{#allOperations}}
+{{#responseType}}
+export const {{operationIdPascal}}ResponseSchema = {{{responseZodExpr}}};
+
+{{/responseType}}
</file context>
|
|
||
| {{#hasSSE}} | ||
| /** {{{description}}} (streaming) */ | ||
| async *{{streamOperationId}}({{#hasParams}}params{{#allParamsOptional}}?{{/allParamsOptional}}: types.{{operationIdPascal}}Params{{/hasParams}}): AsyncGenerator<types.{{{sseType}}}> { |
There was a problem hiding this comment.
P2: The SSE return type is constructed as types.{{{sseType}}} where sseType is set from sseMt.getSchema().getComplexType() or getDataType(). For simple model refs this yields types.Pet, which is fine. However, getDataType() can return composite TypeScript expressions such as Array<Pet>, Pet[], or union types like Pet | Cat. Prepending types. to these produces invalid TypeScript (e.g., types.Array<Pet> or types.Pet | Cat). In contrast, the non-SSE response path uses responseDomainGeneric, which is recursively built by TypescriptExpressZodSupport.resolveResponse() and correctly qualifies nested types. The SSE path bypasses this robust resolution, creating a real bug for streaming endpoints with array, union, or generic event types.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/resources/typescript-express-zod-client/http-client.mustache, line 202:
<comment>The SSE return type is constructed as `types.{{{sseType}}}` where `sseType` is set from `sseMt.getSchema().getComplexType()` or `getDataType()`. For simple model refs this yields `types.Pet`, which is fine. However, `getDataType()` can return composite TypeScript expressions such as `Array<Pet>`, `Pet[]`, or union types like `Pet | Cat`. Prepending `types.` to these produces invalid TypeScript (e.g., `types.Array<Pet>` or `types.Pet | Cat`). In contrast, the non-SSE response path uses `responseDomainGeneric`, which is recursively built by `TypescriptExpressZodSupport.resolveResponse()` and correctly qualifies nested types. The SSE path bypasses this robust resolution, creating a real bug for streaming endpoints with array, union, or generic event types.</comment>
<file context>
@@ -191,15 +191,15 @@ export class Http{{name}} implements types.{{name}} {
- /** {{description}} (streaming) */
- async *{{streamOperationId}}({{#hasParams}}params{{#allParamsOptional}}?{{/allParamsOptional}}: types.{{operationIdPascal}}Params{{/hasParams}}): AsyncGenerator<types.{{sseType}}> {
+ /** {{{description}}} (streaming) */
+ async *{{streamOperationId}}({{#hasParams}}params{{#allParamsOptional}}?{{/allParamsOptional}}: types.{{operationIdPascal}}Params{{/hasParams}}): AsyncGenerator<types.{{{sseType}}}> {
{{#hasQueryParams}}
const query = new URLSearchParams();
</file context>
|
Thanks for the PR cc @TiFu (2017/07) @taxpon (2017/07) @sebastianhaas (2017/07) @kenisteward (2017/07) @Vrolijkx (2017/09) @macjohnny (2018/01) @topce (2018/10) @akehir (2019/07) @petejohansonxo (2019/11) @amakhrov (2020/02) @davidgamero (2022/03) @mkusaka (2022/04) @joscha (2024/10) @dennisameling (2026/02) |
|
thanks for your contribution! note, https://github.com/openapitools/openapi-generator/blob/master/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptClientCodegen.java is a unification of the existing other typescript client generators. |
@wing328 @macjohnny thanks for taking a look at this. Should I avoid creating a the following new code gen files and use the existing file: |
|
@istvanfedak yeah the unified generator should make it easier, see also #6341 |
|
@macjohnny thanks for the context. Looking at the The TypeScript Express Zod client (which is tightly coupled with the TypeScript Express Zod server) differs a a bit. It consumes a framework agnostic fetch implementation allowing the consumer to pass in a fetch like function. It also adds a layer of object validation/filtering with zod before sending the request to the server I'm thinking of adding some branching logic to Would this be a good approach to your comment? Another thing to note is the client doesn't use express but for the naming convention I thought naming them the same would emphasize that tight coupling with the server |
… to typescript-express-zod/petstore.yaml Excludes regenerated samples/ output (committed separately). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NpXMTYcZS92CKpMmtsjSTp
Regenerated output for the unified typescript generator migration and spec relocation to typescript-express-zod/petstore.yaml. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NpXMTYcZS92CKpMmtsjSTp
There was a problem hiding this comment.
10 issues found across 39 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptExpressZodClientUtils.java">
<violation number="1" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptExpressZodClientUtils.java:48">
P1: Path template construction uses raw OpenAPI placeholder text as JavaScript property names instead of the normalized parameter name, causing runtime failures for paths with non-identifier characters (e.g., `user-id` becomes the invalid expression `params.user-id`). Use the corresponding `paramName` from the `pathParams` metadata when building the URL template literal.</violation>
<violation number="2" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptExpressZodClientUtils.java:95">
P1: Operation metadata prepared for client templates drops header and cookie parameters entirely, so the generated HTTP client cannot serialize required headers or cookies into requests. Add `headerParams` and `cookieParams` to the `opInfo` map (similar to how `pathParams` and `queryParams` are added), and include them in the `x-has-no-params` / `hasParams` checks so templates know when these parameters are present.</violation>
</file>
<file name="docs/generators/typescript-express-zod-server.md">
<violation number="1" location="docs/generators/typescript-express-zod-server.md:267">
P1: The Schema Support table incorrectly marks `oneOf` and `Union` as unsupported, but the generator implements discriminated-union handling (including required discriminator properties) as described in the PR. These rows should reflect support.</violation>
<violation number="2" location="docs/generators/typescript-express-zod-server.md:273">
P2: The Security Feature table overstates authentication support. Because the generated server delegates authentication to a user-supplied middleware placeholder and does not generate scheme-specific enforcement, BasicAuth, ApiKey, and OAuth2_Implicit should be marked unsupported.</violation>
<violation number="3" location="docs/generators/typescript-express-zod-server.md:288">
P2: The Wire Format table claims XML is supported, but the generated handler pipeline uses `res.json()` and Zod schemas tailored for JSON. Without XML serialization or content negotiation in the templates, XML should be marked unsupported.</violation>
</file>
<file name="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptExpressZodUtils.java">
<violation number="1" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptExpressZodUtils.java:77">
P2: The `zodExpr()` fallback returns `z.any()` for property shapes that are not arrays, maps, $refs, or primitives. A plain OpenAPI `type: object` (free-form object without typed `additionalProperties`) will miss the map branch when `additionalProperties` is null and fall through to this fallback. `z.any()` is too permissive because it accepts strings, numbers, arrays, and null, whereas `type: object` should be restricted to objects. Consider adding an explicit branch for plain objects before the generic `z.any()` fallback — for example, using `z.object({}).passthrough()` or `z.record(z.string(), z.any())` depending on the desired openness.</violation>
<violation number="2" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptExpressZodUtils.java:126">
P1: The generated Zod schemas currently ignore several standard OpenAPI validation constraints, which allows contract-invalid data to pass runtime validation. `zodExpr()` handles `minimum`/`maximum` and `minLength`/`maxLength`, but drops `pattern` (string regex), `exclusiveMinimum`/`exclusiveMaximum`, `multipleOf`, array `minItems`/`maxItems`/`uniqueItems`, and object property-count bounds. Because this generator advertises runtime Zod validation as a core feature, these omissions directly weaken the server-side request validation and the client-side response validation. Consider extending `zodExpr()` (and the string/number/array/object branches) to emit the corresponding Zod modifiers, e.g., `.regex(...)` for `pattern`, `.gt(...)`/`.lt(...)` for exclusive bounds, `.multipleOf(...)` for `multipleOf`, and `.length(...)` for array bounds where `CodegenProperty` exposes them.</violation>
<violation number="3" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptExpressZodUtils.java:234">
P1: The non-discriminated oneOf detection only handles `$ref` members and silently drops inline schemas. In the consuming templates, `x-union-members` is used to emit `z.union([...])` and the corresponding TypeScript union type, so an all-inline oneOf produces an empty member list and invalid generated code (`z.union([])`). A mixed inline/ref oneOf loses its inline alternatives altogether. Consider extending the member-collection loop to also generate or resolve inline oneOf entries (e.g., inline primitives, arrays, or objects) so the union representation is complete.</violation>
<violation number="4" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptExpressZodUtils.java:489">
P1: Cycle breaking during topological sorting does not produce the `z.lazy()` references that Zod requires for recursive schemas. Self-references are stripped from `dependencies` yet still rendered as plain schema names by `zodExpr`, and mutually recursive models are emitted in an arbitrary order with direct forward references. The generated `schemas.ts` will fail at import time with a ReferenceError because `const` declarations are not hoisted. Consider tracking cyclic dependencies and emitting `z.lazy(() => ...)` for them in both the Java builder and the Mustache template.</violation>
<violation number="5" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptExpressZodUtils.java:517">
P1: Top-level response and request-body Zod schemas are resolved without applying the outer `.nullable()` wrapper, so a nullable response or body is incorrectly rejected by validation. Model properties get `.nullable()` via `addZodVendorExtensions`, but `resolveResponse` and `resolveBody` call `zodExpr` directly and `shapeOf` never wraps for nullability either. The handler template then calls `schemas.OpResponseSchema.parse(payload)` with no null guard. You should extend `resolveResponse` to append `.nullable()` when `rp.isNullable`, and similarly guard `shapeOf`/`resolveBody` for nullable request bodies.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| @@ -0,0 +1,428 @@ | |||
| package org.openapitools.codegen.languages; | |||
There was a problem hiding this comment.
P1: Operation metadata prepared for client templates drops header and cookie parameters entirely, so the generated HTTP client cannot serialize required headers or cookies into requests. Add headerParams and cookieParams to the opInfo map (similar to how pathParams and queryParams are added), and include them in the x-has-no-params / hasParams checks so templates know when these parameters are present.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptExpressZodClientUtils.java, line 95:
<comment>Operation metadata prepared for client templates drops header and cookie parameters entirely, so the generated HTTP client cannot serialize required headers or cookies into requests. Add `headerParams` and `cookieParams` to the `opInfo` map (similar to how `pathParams` and `queryParams` are added), and include them in the `x-has-no-params` / `hasParams` checks so templates know when these parameters are present.</comment>
<file context>
@@ -0,0 +1,428 @@
+ op.vendorExtensions.put("x-body-param-name", bodyParamName);
+ }
+
+ boolean hasNoParams = op.pathParams.isEmpty() && op.queryParams.isEmpty() && op.bodyParam == null;
+ op.vendorExtensions.put("x-has-no-params", hasNoParams);
+
</file context>
| @@ -0,0 +1,428 @@ | |||
| package org.openapitools.codegen.languages; | |||
There was a problem hiding this comment.
P1: Path template construction uses raw OpenAPI placeholder text as JavaScript property names instead of the normalized parameter name, causing runtime failures for paths with non-identifier characters (e.g., user-id becomes the invalid expression params.user-id). Use the corresponding paramName from the pathParams metadata when building the URL template literal.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptExpressZodClientUtils.java, line 48:
<comment>Path template construction uses raw OpenAPI placeholder text as JavaScript property names instead of the normalized parameter name, causing runtime failures for paths with non-identifier characters (e.g., `user-id` becomes the invalid expression `params.user-id`). Use the corresponding `paramName` from the `pathParams` metadata when building the URL template literal.</comment>
<file context>
@@ -0,0 +1,428 @@
+ for (CodegenOperation op : ops) {
+ // Build URL template for JS template literals:
+ // /recipe/{recipeId} -> /recipe/${encodeURIComponent(params.recipeId)}
+ String urlTemplate = op.path.replaceAll(
+ "\\{([^}]+)\\}",
+ "\\${encodeURIComponent(String(params.$1))}"
</file context>
| |Union|✗|OAS3 | ||
| |allOf|✗|OAS2,OAS3 | ||
| |anyOf|✗|OAS3 | ||
| |oneOf|✗|OAS3 |
There was a problem hiding this comment.
P1: The Schema Support table incorrectly marks oneOf and Union as unsupported, but the generator implements discriminated-union handling (including required discriminator properties) as described in the PR. These rows should reflect support.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/generators/typescript-express-zod-server.md, line 267:
<comment>The Schema Support table incorrectly marks `oneOf` and `Union` as unsupported, but the generator implements discriminated-union handling (including required discriminator properties) as described in the PR. These rows should reflect support.</comment>
<file context>
@@ -0,0 +1,290 @@
+|Union|✗|OAS3
+|allOf|✗|OAS2,OAS3
+|anyOf|✗|OAS3
+|oneOf|✗|OAS3
+|not|✗|OAS3
+
</file context>
| } | ||
|
|
||
| // Primitive types. | ||
| if (prop.isInteger || prop.isLong) { |
There was a problem hiding this comment.
P1: The generated Zod schemas currently ignore several standard OpenAPI validation constraints, which allows contract-invalid data to pass runtime validation. zodExpr() handles minimum/maximum and minLength/maxLength, but drops pattern (string regex), exclusiveMinimum/exclusiveMaximum, multipleOf, array minItems/maxItems/uniqueItems, and object property-count bounds. Because this generator advertises runtime Zod validation as a core feature, these omissions directly weaken the server-side request validation and the client-side response validation. Consider extending zodExpr() (and the string/number/array/object branches) to emit the corresponding Zod modifiers, e.g., .regex(...) for pattern, .gt(...)/.lt(...) for exclusive bounds, .multipleOf(...) for multipleOf, and .length(...) for array bounds where CodegenProperty exposes them.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptExpressZodUtils.java, line 126:
<comment>The generated Zod schemas currently ignore several standard OpenAPI validation constraints, which allows contract-invalid data to pass runtime validation. `zodExpr()` handles `minimum`/`maximum` and `minLength`/`maxLength`, but drops `pattern` (string regex), `exclusiveMinimum`/`exclusiveMaximum`, `multipleOf`, array `minItems`/`maxItems`/`uniqueItems`, and object property-count bounds. Because this generator advertises runtime Zod validation as a core feature, these omissions directly weaken the server-side request validation and the client-side response validation. Consider extending `zodExpr()` (and the string/number/array/object branches) to emit the corresponding Zod modifiers, e.g., `.regex(...)` for `pattern`, `.gt(...)`/`.lt(...)` for exclusive bounds, `.multipleOf(...)` for `multipleOf`, and `.length(...)` for array bounds where `CodegenProperty` exposes them.</comment>
<file context>
@@ -0,0 +1,645 @@
+ }
+
+ // Primitive types.
+ if (prop.isInteger || prop.isLong) {
+ StringBuilder sb = new StringBuilder("z.coerce.number().int()");
+ if (prop.minimum != null) sb.append(".gte(").append(prop.minimum).append(")");
</file context>
| if (origSchema != null && origSchema.getOneOf() != null && !origSchema.getOneOf().isEmpty()) { | ||
| cm.vendorExtensions.put("x-is-union", true); | ||
|
|
||
| // Collect member classnames |
There was a problem hiding this comment.
P1: The non-discriminated oneOf detection only handles $ref members and silently drops inline schemas. In the consuming templates, x-union-members is used to emit z.union([...]) and the corresponding TypeScript union type, so an all-inline oneOf produces an empty member list and invalid generated code (z.union([])). A mixed inline/ref oneOf loses its inline alternatives altogether. Consider extending the member-collection loop to also generate or resolve inline oneOf entries (e.g., inline primitives, arrays, or objects) so the union representation is complete.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptExpressZodUtils.java, line 234:
<comment>The non-discriminated oneOf detection only handles `$ref` members and silently drops inline schemas. In the consuming templates, `x-union-members` is used to emit `z.union([...])` and the corresponding TypeScript union type, so an all-inline oneOf produces an empty member list and invalid generated code (`z.union([])`). A mixed inline/ref oneOf loses its inline alternatives altogether. Consider extending the member-collection loop to also generate or resolve inline oneOf entries (e.g., inline primitives, arrays, or objects) so the union representation is complete.</comment>
<file context>
@@ -0,0 +1,645 @@
+ if (origSchema != null && origSchema.getOneOf() != null && !origSchema.getOneOf().isEmpty()) {
+ cm.vendorExtensions.put("x-is-union", true);
+
+ // Collect member classnames
+ List<String> memberNames = new ArrayList<>();
+ for (Object memberObj : origSchema.getOneOf()) {
</file context>
| */ | ||
| static Resolved resolveResponse(CodegenOperation op, Direction dir) { | ||
| CodegenProperty rp = op.returnProperty; | ||
| String zod = zodExpr(rp); |
There was a problem hiding this comment.
P1: Top-level response and request-body Zod schemas are resolved without applying the outer .nullable() wrapper, so a nullable response or body is incorrectly rejected by validation. Model properties get .nullable() via addZodVendorExtensions, but resolveResponse and resolveBody call zodExpr directly and shapeOf never wraps for nullability either. The handler template then calls schemas.OpResponseSchema.parse(payload) with no null guard. You should extend resolveResponse to append .nullable() when rp.isNullable, and similarly guard shapeOf/resolveBody for nullable request bodies.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptExpressZodUtils.java, line 517:
<comment>Top-level response and request-body Zod schemas are resolved without applying the outer `.nullable()` wrapper, so a nullable response or body is incorrectly rejected by validation. Model properties get `.nullable()` via `addZodVendorExtensions`, but `resolveResponse` and `resolveBody` call `zodExpr` directly and `shapeOf` never wraps for nullability either. The handler template then calls `schemas.OpResponseSchema.parse(payload)` with no null guard. You should extend `resolveResponse` to append `.nullable()` when `rp.isNullable`, and similarly guard `shapeOf`/`resolveBody` for nullable request bodies.</comment>
<file context>
@@ -0,0 +1,645 @@
+ */
+ static Resolved resolveResponse(CodegenOperation op, Direction dir) {
+ CodegenProperty rp = op.returnProperty;
+ String zod = zodExpr(rp);
+ if (rp == null) {
+ // DefaultCodegen assigns returnType and returnProperty together, so this is
</file context>
| Map<String, Set<String>> dependencies, | ||
| Set<String> visited, Set<String> visiting, List<Object> sorted) { | ||
| if (visited.contains(name)) return; | ||
| if (visiting.contains(name)) return; // cycle detected, break it |
There was a problem hiding this comment.
P1: Cycle breaking during topological sorting does not produce the z.lazy() references that Zod requires for recursive schemas. Self-references are stripped from dependencies yet still rendered as plain schema names by zodExpr, and mutually recursive models are emitted in an arbitrary order with direct forward references. The generated schemas.ts will fail at import time with a ReferenceError because const declarations are not hoisted. Consider tracking cyclic dependencies and emitting z.lazy(() => ...) for them in both the Java builder and the Mustache template.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptExpressZodUtils.java, line 489:
<comment>Cycle breaking during topological sorting does not produce the `z.lazy()` references that Zod requires for recursive schemas. Self-references are stripped from `dependencies` yet still rendered as plain schema names by `zodExpr`, and mutually recursive models are emitted in an arbitrary order with direct forward references. The generated `schemas.ts` will fail at import time with a ReferenceError because `const` declarations are not hoisted. Consider tracking cyclic dependencies and emitting `z.lazy(() => ...)` for them in both the Java builder and the Mustache template.</comment>
<file context>
@@ -0,0 +1,645 @@
+ Map<String, Set<String>> dependencies,
+ Set<String> visited, Set<String> visiting, List<Object> sorted) {
+ if (visited.contains(name)) return;
+ if (visiting.contains(name)) return; // cycle detected, break it
+ visiting.add(name);
+
</file context>
| ### Security Feature | ||
| | Name | Supported | Defined By | | ||
| | ---- | --------- | ---------- | | ||
| |BasicAuth|✓|OAS2,OAS3 |
There was a problem hiding this comment.
P2: The Security Feature table overstates authentication support. Because the generated server delegates authentication to a user-supplied middleware placeholder and does not generate scheme-specific enforcement, BasicAuth, ApiKey, and OAuth2_Implicit should be marked unsupported.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/generators/typescript-express-zod-server.md, line 273:
<comment>The Security Feature table overstates authentication support. Because the generated server delegates authentication to a user-supplied middleware placeholder and does not generate scheme-specific enforcement, BasicAuth, ApiKey, and OAuth2_Implicit should be marked unsupported.</comment>
<file context>
@@ -0,0 +1,290 @@
+### Security Feature
+| Name | Supported | Defined By |
+| ---- | --------- | ---------- |
+|BasicAuth|✓|OAS2,OAS3
+|ApiKey|✓|OAS2,OAS3
+|OpenIDConnect|✗|OAS3
</file context>
| | Name | Supported | Defined By | | ||
| | ---- | --------- | ---------- | | ||
| |JSON|✓|OAS2,OAS3 | ||
| |XML|✓|OAS2,OAS3 |
There was a problem hiding this comment.
P2: The Wire Format table claims XML is supported, but the generated handler pipeline uses res.json() and Zod schemas tailored for JSON. Without XML serialization or content negotiation in the templates, XML should be marked unsupported.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/generators/typescript-express-zod-server.md, line 288:
<comment>The Wire Format table claims XML is supported, but the generated handler pipeline uses `res.json()` and Zod schemas tailored for JSON. Without XML serialization or content negotiation in the templates, XML should be marked unsupported.</comment>
<file context>
@@ -0,0 +1,290 @@
+| Name | Supported | Defined By |
+| ---- | --------- | ---------- |
+|JSON|✓|OAS2,OAS3
+|XML|✓|OAS2,OAS3
+|PROTOBUF|✗|ToolingExtension
+|Custom|✗|OAS2,OAS3
</file context>
| |XML|✓|OAS2,OAS3 | |
| +|XML|✗|OAS2,OAS3 |
| */ | ||
| static String zodExpr(CodegenProperty prop) { | ||
| if (prop == null) { | ||
| return "z.any()"; |
There was a problem hiding this comment.
P2: The zodExpr() fallback returns z.any() for property shapes that are not arrays, maps, $refs, or primitives. A plain OpenAPI type: object (free-form object without typed additionalProperties) will miss the map branch when additionalProperties is null and fall through to this fallback. z.any() is too permissive because it accepts strings, numbers, arrays, and null, whereas type: object should be restricted to objects. Consider adding an explicit branch for plain objects before the generic z.any() fallback — for example, using z.object({}).passthrough() or z.record(z.string(), z.any()) depending on the desired openness.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptExpressZodUtils.java, line 77:
<comment>The `zodExpr()` fallback returns `z.any()` for property shapes that are not arrays, maps, $refs, or primitives. A plain OpenAPI `type: object` (free-form object without typed `additionalProperties`) will miss the map branch when `additionalProperties` is null and fall through to this fallback. `z.any()` is too permissive because it accepts strings, numbers, arrays, and null, whereas `type: object` should be restricted to objects. Consider adding an explicit branch for plain objects before the generic `z.any()` fallback — for example, using `z.object({}).passthrough()` or `z.record(z.string(), z.any())` depending on the desired openness.</comment>
<file context>
@@ -0,0 +1,645 @@
+ */
+ static String zodExpr(CodegenProperty prop) {
+ if (prop == null) {
+ return "z.any()";
+ }
+
</file context>
|
@istvanfedak i wont have time to look into it in more detail, but i guess now that you have the context you will make the right choice for your new generator ;-) let me know when its ready to be merged |
|
@macjohnny thanks! I will let you know once its ready to merge. I'll finish polishing the generator |
|
@macjohnny I'm thinking of porting this over to OAS 3.1 would openapi-generator support Open API Spec 3.1? I see some generators use it |
[typescript-express-zod] Add server and client generators for Express + Zod
PR checklist
[community] [typescript-express-zod] Add server and client generatorsmaster./bin/generate-samples.sh ./bin/configs/typescript-express-zod-*to update sample outputDescription
These generators target teams building Express APIs who want their OpenAPI spec to be the single source of truth for:
getRouter()function that wires routes, middleware, validation, Swagger UI, and error handlingGenerated code has zero hand-written boilerplate. You implement service interfaces; the generator handles routing, validation, serialization, and error responses.
Key features
asynchandler supportcompact()stripsundefinedfieldsgetRouter()accepts service factories, middleware config, and handler overrides/from the embedded OpenAPI schema — no separate docs server needed_onlySwaggerUIand_exceptSwaggerUIscopesAuthService,OrganizationsService)typescript-express-zod-clientgeneratesHttpAuthService,HttpOrganizationsServiceclasses that share the same interfacesValidationErrorsError,HandledExceptionError,UnhandledExceptionErrorwith type guardsUsage
The generator produces one interface per OpenAPI tag. You implement them:
How the request/response pipeline works
Request
→ Express route matched by router-factory.ts
→ Middleware chain (global + per-endpoint)
→ Handler extracts params from req.body / req.params
→ DTO mapper converts wire format → domain types
→ Zod schema validates the params
→ Service method executes your business logic
→ Error check: if result contains { errors: [...] }, send error response
→ DTO mapper converts domain response → wire format
→ res.status(200).json(responseDto)
→ Post-send: Zod validates response against schema (catches contract drift)
Key design decisions
Why Zod instead of class-validator / class-transformer?
Zod schemas are generated directly from the OpenAPI spec and validate both request and response payloads at runtime. After sending the response, the handler re-validates it against the schema — this catches contract drift between your implementation and the spec early (as a 500-level validation error if headers were already sent, or a 400 if not).
Why a separate DTO layer?
The DTO types represent the exact wire format (e.g. dates as strings). Domain types may diverge as the spec evolves. The compact() utility in mappers strips undefined values to produce clean JSON payloads. Having both layers lets you evolve the internal representation without breaking the API contract.
Why service factory functions (req, res) => Service instead of static instances?
This enables request-scoped dependency injection. Each request gets a fresh service instance with access to the current request context (auth headers, tenant ID, etc.) without requiring a DI framework.
Why Express v5?
Express 5 supports async route handlers natively and has improved path matching. The generated handlers are all async and rely on Express 5's promise-based error propagation.
Why OPTIONS handlers and 405 Method Not Allowed?
Every route explicitly declares which HTTP methods it supports. Unsupported methods return 405 with an Allow header — proper REST semantics out of the box.
Summary by cubic
Adds a new server generator
typescript-express-zod-serverand migrates the client to the unifiedtypescriptgenerator viaframework: express-zod. It produces a typed Express v5 server and matching client with runtimezodv4 validation and SSE support. Samples are regenerated and CI runs on Node 20/22.New Features
typescript-express-zod-serverwith typed router factory, middleware hooks, SSE streaming, and Swagger UI.typescriptwithframework: express-zodto generate a matching HTTP client with DTO mappers and streaming helpers.zodv4 for request/response validation; enums from allowable values; discriminator properties required.META-INF/services; new docs page; index exports included.Bug Fixes
ZodErroron unknown discriminators (server input) and are import‑gated.dataType; optional request bodies honored; primitive body fallbacks use standard Zod primitives; fallbacks for models without properties.sendResponse(); client skips JSON on no‑content; stronger error type guards; escape<in embedded Swagger UI schema.buildZodTypefallbacks aligned.Written for commit f8bafb1. Summary will update on new commits.