From 8a2531d79fb6934f58d2538e331a5c771df87760 Mon Sep 17 00:00:00 2001 From: Anna Bocharova Date: Tue, 2 Dec 2025 19:18:35 +0100 Subject: [PATCH 01/13] Ref: single WeakMap and single check method for the Diagnostics. --- express-zod-api/src/diagnostics.ts | 129 ++++++++++++++++------------- express-zod-api/src/routing.ts | 3 +- 2 files changed, 73 insertions(+), 59 deletions(-) diff --git a/express-zod-api/src/diagnostics.ts b/express-zod-api/src/diagnostics.ts index 024f4ffcfc..2ff92a90b4 100644 --- a/express-zod-api/src/diagnostics.ts +++ b/express-zod-api/src/diagnostics.ts @@ -8,77 +8,92 @@ import { flattenIO } from "./json-schema-helpers"; import { ActualLogger } from "./logger-helpers"; export class Diagnostics { - #verifiedEndpoints = new WeakSet(); - #verifiedPaths = new WeakMap< + #verified = new WeakMap< AbstractEndpoint, - { flat: ReturnType; paths: string[] } + { + schemaChecked: boolean; + flat?: ReturnType; + paths: string[]; + } >(); constructor(protected logger: ActualLogger) {} - public checkSchema(endpoint: AbstractEndpoint, ctx: FlatObject): void { - if (this.#verifiedEndpoints.has(endpoint)) return; - for (const dir of ["input", "output"] as const) { - const stack = [ - z.toJSONSchema(endpoint[`${dir}Schema`], { unrepresentable: "any" }), - ]; - while (stack.length > 0) { - const entry = stack.shift()!; - if (entry.type && entry.type !== "object") - this.logger.warn(`Endpoint ${dir} schema is not object-based`, ctx); - for (const prop of ["allOf", "oneOf", "anyOf"] as const) - if (entry[prop]) stack.push(...entry[prop]); - } + public check( + endpoint: AbstractEndpoint, + ctx: FlatObject, + path?: string, + ): void { + let ref = this.#verified.get(endpoint); + if (!ref) { + ref = { schemaChecked: false, paths: [] }; + this.#verified.set(endpoint, ref); } - if (endpoint.requestType === "json") { - const reason = findJsonIncompatible(endpoint.inputSchema, "input"); - if (reason) { - this.logger.warn( - "The final input schema of the endpoint contains an unsupported JSON payload type.", - Object.assign(ctx, { reason }), - ); + + // Schema check + if (!ref.schemaChecked) { + for (const dir of ["input", "output"] as const) { + const stack = [ + z.toJSONSchema(endpoint[`${dir}Schema`], { unrepresentable: "any" }), + ]; + while (stack.length > 0) { + const entry = stack.shift()!; + if (entry.type && entry.type !== "object") { + this.logger.warn( + `Endpoint ${dir} schema is not object-based`, + Object.assign(ctx, { path }), + ); + } + for (const prop of ["allOf", "oneOf", "anyOf"] as const) + if (entry[prop]) stack.push(...entry[prop]); + } } - } - for (const variant of responseVariants) { - for (const { mimeTypes, schema } of endpoint.getResponses(variant)) { - if (!mimeTypes?.includes(contentTypes.json)) continue; - const reason = findJsonIncompatible(schema, "output"); + if (endpoint.requestType === "json") { + const reason = findJsonIncompatible(endpoint.inputSchema, "input"); if (reason) { this.logger.warn( - `The final ${variant} response schema of the endpoint contains an unsupported JSON payload type.`, - Object.assign(ctx, { reason }), + "The final input schema of the endpoint contains an unsupported JSON payload type.", + Object.assign(ctx, { path, reason }), ); } } + for (const variant of responseVariants) { + for (const { mimeTypes, schema } of endpoint.getResponses(variant)) { + if (!mimeTypes?.includes(contentTypes.json)) continue; + const reason = findJsonIncompatible(schema, "output"); + if (reason) { + this.logger.warn( + `The final ${variant} response schema of the endpoint contains an unsupported JSON payload type.`, + Object.assign(ctx, { path, reason }), + ); + } + } + } + ref.schemaChecked = true; } - this.#verifiedEndpoints.add(endpoint); - } - public checkPathParams( - path: string, - endpoint: AbstractEndpoint, - ctx: FlatObject, - ): void { - const ref = this.#verifiedPaths.get(endpoint); - if (ref?.paths.includes(path)) return; - const params = getRoutePathParams(path); - if (params.length === 0) return; // next statement can be expensive - const flat = - ref?.flat || - flattenIO( - z.toJSONSchema(endpoint.inputSchema, { - unrepresentable: "any", - io: "input", - }), - ); - for (const param of params) { - if (param in flat.properties) continue; - this.logger.warn( - "The input schema of the endpoint is most likely missing the parameter of the path it's assigned to.", - Object.assign(ctx, { path, param }), - ); + // Path params check + if (path && !ref.paths.includes(path)) { + const params = getRoutePathParams(path); + if (params.length > 0) { + const flat = + ref.flat || + flattenIO( + z.toJSONSchema(endpoint.inputSchema, { + unrepresentable: "any", + io: "input", + }), + ); + ref.flat = flat; + for (const param of params) { + if (param in flat.properties) continue; + this.logger.warn( + "The input schema of the endpoint is most likely missing the parameter of the path it's assigned to.", + Object.assign(ctx, { path, param }), + ); + } + } + ref.paths.push(path); } - if (ref) ref.paths.push(path); - else this.#verifiedPaths.set(endpoint, { flat, paths: [path] }); } } diff --git a/express-zod-api/src/routing.ts b/express-zod-api/src/routing.ts index d12261d71e..b0a6ca5f1e 100644 --- a/express-zod-api/src/routing.ts +++ b/express-zod-api/src/routing.ts @@ -70,8 +70,7 @@ const collectSiblings = ({ const doc = isProduction() ? undefined : new Diagnostics(getLogger()); const familiar = new Map(); const onEndpoint: OnEndpoint = (method, path, endpoint) => { - doc?.checkSchema(endpoint, { path, method }); - doc?.checkPathParams(path, endpoint, { method }); + doc?.check(endpoint, { method }, path); const matchingParsers = parsers?.[endpoint.requestType] || []; const value = R.pair(matchingParsers, endpoint); if (!familiar.has(path)) From 54a1086bd2aaa1239b963c18c2f5ef4ea39a379f Mon Sep 17 00:00:00 2001 From: Anna Bocharova Date: Tue, 2 Dec 2025 19:22:17 +0100 Subject: [PATCH 02/13] Restore required path and inverting condition. --- express-zod-api/src/diagnostics.ts | 41 +++++++++++++++--------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/express-zod-api/src/diagnostics.ts b/express-zod-api/src/diagnostics.ts index 2ff92a90b4..d0a947c228 100644 --- a/express-zod-api/src/diagnostics.ts +++ b/express-zod-api/src/diagnostics.ts @@ -22,7 +22,7 @@ export class Diagnostics { public check( endpoint: AbstractEndpoint, ctx: FlatObject, - path?: string, + path: string, ): void { let ref = this.#verified.get(endpoint); if (!ref) { @@ -73,27 +73,26 @@ export class Diagnostics { } // Path params check - if (path && !ref.paths.includes(path)) { - const params = getRoutePathParams(path); - if (params.length > 0) { - const flat = - ref.flat || - flattenIO( - z.toJSONSchema(endpoint.inputSchema, { - unrepresentable: "any", - io: "input", - }), - ); - ref.flat = flat; - for (const param of params) { - if (param in flat.properties) continue; - this.logger.warn( - "The input schema of the endpoint is most likely missing the parameter of the path it's assigned to.", - Object.assign(ctx, { path, param }), - ); - } + if (ref.paths.includes(path)) return; + const params = getRoutePathParams(path); + if (params.length > 0) { + const flat = + ref.flat || + flattenIO( + z.toJSONSchema(endpoint.inputSchema, { + unrepresentable: "any", + io: "input", + }), + ); + ref.flat = flat; + for (const param of params) { + if (param in flat.properties) continue; + this.logger.warn( + "The input schema of the endpoint is most likely missing the parameter of the path it's assigned to.", + Object.assign(ctx, { path, param }), + ); } - ref.paths.push(path); } + ref.paths.push(path); } } From c4d2619ac9ed2974ff1a3122b0f7322d23912fd7 Mon Sep 17 00:00:00 2001 From: Anna Bocharova Date: Tue, 2 Dec 2025 19:25:35 +0100 Subject: [PATCH 03/13] Restoring original shortcut to avoid expensive path params check. --- express-zod-api/src/diagnostics.ts | 33 +++++++++++++++--------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/express-zod-api/src/diagnostics.ts b/express-zod-api/src/diagnostics.ts index d0a947c228..c40c317805 100644 --- a/express-zod-api/src/diagnostics.ts +++ b/express-zod-api/src/diagnostics.ts @@ -75,23 +75,22 @@ export class Diagnostics { // Path params check if (ref.paths.includes(path)) return; const params = getRoutePathParams(path); - if (params.length > 0) { - const flat = - ref.flat || - flattenIO( - z.toJSONSchema(endpoint.inputSchema, { - unrepresentable: "any", - io: "input", - }), - ); - ref.flat = flat; - for (const param of params) { - if (param in flat.properties) continue; - this.logger.warn( - "The input schema of the endpoint is most likely missing the parameter of the path it's assigned to.", - Object.assign(ctx, { path, param }), - ); - } + if (params.length === 0) return; // next statement can be expensive + const flat = + ref.flat || + flattenIO( + z.toJSONSchema(endpoint.inputSchema, { + unrepresentable: "any", + io: "input", + }), + ); + ref.flat = flat; + for (const param of params) { + if (param in flat.properties) continue; + this.logger.warn( + "The input schema of the endpoint is most likely missing the parameter of the path it's assigned to.", + Object.assign(ctx, { path, param }), + ); } ref.paths.push(path); } From f282976f5a488c95fe58ce5d07de689ad28d3758 Mon Sep 17 00:00:00 2001 From: Anna Bocharova Date: Tue, 2 Dec 2025 19:31:20 +0100 Subject: [PATCH 04/13] Consistent arguments similar to OnEndpoint. --- express-zod-api/src/diagnostics.ts | 23 ++++++++++------------- express-zod-api/src/routing.ts | 2 +- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/express-zod-api/src/diagnostics.ts b/express-zod-api/src/diagnostics.ts index c40c317805..8727e78028 100644 --- a/express-zod-api/src/diagnostics.ts +++ b/express-zod-api/src/diagnostics.ts @@ -1,11 +1,12 @@ import { z } from "zod"; import { responseVariants } from "./api-response"; -import { FlatObject, getRoutePathParams } from "./common-helpers"; +import { getRoutePathParams } from "./common-helpers"; import { contentTypes } from "./content-type"; import { findJsonIncompatible } from "./deep-checks"; import { AbstractEndpoint } from "./endpoint"; import { flattenIO } from "./json-schema-helpers"; import { ActualLogger } from "./logger-helpers"; +import { Method } from "./method"; export class Diagnostics { #verified = new WeakMap< @@ -19,11 +20,7 @@ export class Diagnostics { constructor(protected logger: ActualLogger) {} - public check( - endpoint: AbstractEndpoint, - ctx: FlatObject, - path: string, - ): void { + public check(method: Method, path: string, endpoint: AbstractEndpoint): void { let ref = this.#verified.get(endpoint); if (!ref) { ref = { schemaChecked: false, paths: [] }; @@ -39,10 +36,10 @@ export class Diagnostics { while (stack.length > 0) { const entry = stack.shift()!; if (entry.type && entry.type !== "object") { - this.logger.warn( - `Endpoint ${dir} schema is not object-based`, - Object.assign(ctx, { path }), - ); + this.logger.warn(`Endpoint ${dir} schema is not object-based`, { + method, + path, + }); } for (const prop of ["allOf", "oneOf", "anyOf"] as const) if (entry[prop]) stack.push(...entry[prop]); @@ -53,7 +50,7 @@ export class Diagnostics { if (reason) { this.logger.warn( "The final input schema of the endpoint contains an unsupported JSON payload type.", - Object.assign(ctx, { path, reason }), + { method, path, reason }, ); } } @@ -64,7 +61,7 @@ export class Diagnostics { if (reason) { this.logger.warn( `The final ${variant} response schema of the endpoint contains an unsupported JSON payload type.`, - Object.assign(ctx, { path, reason }), + { method, path, reason }, ); } } @@ -89,7 +86,7 @@ export class Diagnostics { if (param in flat.properties) continue; this.logger.warn( "The input schema of the endpoint is most likely missing the parameter of the path it's assigned to.", - Object.assign(ctx, { path, param }), + { method, path, param }, ); } ref.paths.push(path); diff --git a/express-zod-api/src/routing.ts b/express-zod-api/src/routing.ts index b0a6ca5f1e..c9b5d7ceab 100644 --- a/express-zod-api/src/routing.ts +++ b/express-zod-api/src/routing.ts @@ -70,7 +70,7 @@ const collectSiblings = ({ const doc = isProduction() ? undefined : new Diagnostics(getLogger()); const familiar = new Map(); const onEndpoint: OnEndpoint = (method, path, endpoint) => { - doc?.check(endpoint, { method }, path); + doc?.check(method, path, endpoint); const matchingParsers = parsers?.[endpoint.requestType] || []; const value = R.pair(matchingParsers, endpoint); if (!familiar.has(path)) From cda76d0e80971367996d4619ea87b0a6ebc284f6 Mon Sep 17 00:00:00 2001 From: Anna Bocharova Date: Tue, 2 Dec 2025 19:59:14 +0100 Subject: [PATCH 05/13] Extracting two private methods to retain early exits. --- express-zod-api/src/diagnostics.ts | 108 ++++++++++++++++------------- 1 file changed, 60 insertions(+), 48 deletions(-) diff --git a/express-zod-api/src/diagnostics.ts b/express-zod-api/src/diagnostics.ts index 8727e78028..7e572f2da0 100644 --- a/express-zod-api/src/diagnostics.ts +++ b/express-zod-api/src/diagnostics.ts @@ -8,68 +8,70 @@ import { flattenIO } from "./json-schema-helpers"; import { ActualLogger } from "./logger-helpers"; import { Method } from "./method"; +interface Cache { + schemaChecked: boolean; + flat?: ReturnType; + paths: string[]; +} + export class Diagnostics { - #verified = new WeakMap< - AbstractEndpoint, - { - schemaChecked: boolean; - flat?: ReturnType; - paths: string[]; - } - >(); + #verified = new WeakMap(); constructor(protected logger: ActualLogger) {} - public check(method: Method, path: string, endpoint: AbstractEndpoint): void { - let ref = this.#verified.get(endpoint); - if (!ref) { - ref = { schemaChecked: false, paths: [] }; - this.#verified.set(endpoint, ref); - } - - // Schema check - if (!ref.schemaChecked) { - for (const dir of ["input", "output"] as const) { - const stack = [ - z.toJSONSchema(endpoint[`${dir}Schema`], { unrepresentable: "any" }), - ]; - while (stack.length > 0) { - const entry = stack.shift()!; - if (entry.type && entry.type !== "object") { - this.logger.warn(`Endpoint ${dir} schema is not object-based`, { - method, - path, - }); - } - for (const prop of ["allOf", "oneOf", "anyOf"] as const) - if (entry[prop]) stack.push(...entry[prop]); + #checkSchema( + ref: Cache, + method: Method, + path: string, + endpoint: AbstractEndpoint, + ): void { + if (ref.schemaChecked) return; + for (const dir of ["input", "output"] as const) { + const stack = [ + z.toJSONSchema(endpoint[`${dir}Schema`], { unrepresentable: "any" }), + ]; + while (stack.length > 0) { + const entry = stack.shift()!; + if (entry.type && entry.type !== "object") { + this.logger.warn(`Endpoint ${dir} schema is not object-based`, { + method, + path, + }); } + for (const prop of ["allOf", "oneOf", "anyOf"] as const) + if (entry[prop]) stack.push(...entry[prop]); + } + } + if (endpoint.requestType === "json") { + const reason = findJsonIncompatible(endpoint.inputSchema, "input"); + if (reason) { + this.logger.warn( + "The final input schema of the endpoint contains an unsupported JSON payload type.", + { method, path, reason }, + ); } - if (endpoint.requestType === "json") { - const reason = findJsonIncompatible(endpoint.inputSchema, "input"); + } + for (const variant of responseVariants) { + for (const { mimeTypes, schema } of endpoint.getResponses(variant)) { + if (!mimeTypes?.includes(contentTypes.json)) continue; + const reason = findJsonIncompatible(schema, "output"); if (reason) { this.logger.warn( - "The final input schema of the endpoint contains an unsupported JSON payload type.", + `The final ${variant} response schema of the endpoint contains an unsupported JSON payload type.`, { method, path, reason }, ); } } - for (const variant of responseVariants) { - for (const { mimeTypes, schema } of endpoint.getResponses(variant)) { - if (!mimeTypes?.includes(contentTypes.json)) continue; - const reason = findJsonIncompatible(schema, "output"); - if (reason) { - this.logger.warn( - `The final ${variant} response schema of the endpoint contains an unsupported JSON payload type.`, - { method, path, reason }, - ); - } - } - } - ref.schemaChecked = true; } + ref.schemaChecked = true; + } - // Path params check + #checkPath( + ref: Cache, + method: Method, + path: string, + endpoint: AbstractEndpoint, + ): void { if (ref.paths.includes(path)) return; const params = getRoutePathParams(path); if (params.length === 0) return; // next statement can be expensive @@ -91,4 +93,14 @@ export class Diagnostics { } ref.paths.push(path); } + + public check(method: Method, path: string, endpoint: AbstractEndpoint): void { + let ref = this.#verified.get(endpoint); + if (!ref) { + ref = { schemaChecked: false, paths: [] }; + this.#verified.set(endpoint, ref); + } + this.#checkSchema(ref, method, path, endpoint); + this.#checkPath(ref, method, path, endpoint); + } } From bda3e9d7f3c5ad16b3075e40cb3b040216e7924d Mon Sep 17 00:00:00 2001 From: Anna Bocharova Date: Tue, 2 Dec 2025 20:00:51 +0100 Subject: [PATCH 06/13] Ref: removing redundant const. --- express-zod-api/src/diagnostics.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/express-zod-api/src/diagnostics.ts b/express-zod-api/src/diagnostics.ts index 7e572f2da0..8ffe3d160d 100644 --- a/express-zod-api/src/diagnostics.ts +++ b/express-zod-api/src/diagnostics.ts @@ -75,7 +75,7 @@ export class Diagnostics { if (ref.paths.includes(path)) return; const params = getRoutePathParams(path); if (params.length === 0) return; // next statement can be expensive - const flat = + ref.flat = ref.flat || flattenIO( z.toJSONSchema(endpoint.inputSchema, { @@ -83,9 +83,8 @@ export class Diagnostics { io: "input", }), ); - ref.flat = flat; for (const param of params) { - if (param in flat.properties) continue; + if (param in ref.flat.properties) continue; this.logger.warn( "The input schema of the endpoint is most likely missing the parameter of the path it's assigned to.", { method, path, param }, From 7380ccef37ec33c22f94b019979c701f30e5e610 Mon Sep 17 00:00:00 2001 From: Anna Bocharova Date: Tue, 2 Dec 2025 20:03:19 +0100 Subject: [PATCH 07/13] Ref: conventional naming for bool. --- express-zod-api/src/diagnostics.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/express-zod-api/src/diagnostics.ts b/express-zod-api/src/diagnostics.ts index 8ffe3d160d..aa9827f6b4 100644 --- a/express-zod-api/src/diagnostics.ts +++ b/express-zod-api/src/diagnostics.ts @@ -9,7 +9,7 @@ import { ActualLogger } from "./logger-helpers"; import { Method } from "./method"; interface Cache { - schemaChecked: boolean; + hasValidSchema: boolean; flat?: ReturnType; paths: string[]; } @@ -25,7 +25,7 @@ export class Diagnostics { path: string, endpoint: AbstractEndpoint, ): void { - if (ref.schemaChecked) return; + if (ref.hasValidSchema) return; for (const dir of ["input", "output"] as const) { const stack = [ z.toJSONSchema(endpoint[`${dir}Schema`], { unrepresentable: "any" }), @@ -63,7 +63,7 @@ export class Diagnostics { } } } - ref.schemaChecked = true; + ref.hasValidSchema = true; } #checkPath( @@ -96,7 +96,7 @@ export class Diagnostics { public check(method: Method, path: string, endpoint: AbstractEndpoint): void { let ref = this.#verified.get(endpoint); if (!ref) { - ref = { schemaChecked: false, paths: [] }; + ref = { hasValidSchema: false, paths: [] }; this.#verified.set(endpoint, ref); } this.#checkSchema(ref, method, path, endpoint); From e48c894aaf46ae403c353497225de5acbb35dc4f Mon Sep 17 00:00:00 2001 From: Anna Bocharova Date: Tue, 2 Dec 2025 22:44:09 +0100 Subject: [PATCH 08/13] Ref: non-nullable assignment. --- express-zod-api/src/diagnostics.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/express-zod-api/src/diagnostics.ts b/express-zod-api/src/diagnostics.ts index aa9827f6b4..5f89ad071a 100644 --- a/express-zod-api/src/diagnostics.ts +++ b/express-zod-api/src/diagnostics.ts @@ -66,7 +66,7 @@ export class Diagnostics { ref.hasValidSchema = true; } - #checkPath( + #checkPathParams( ref: Cache, method: Method, path: string, @@ -75,14 +75,12 @@ export class Diagnostics { if (ref.paths.includes(path)) return; const params = getRoutePathParams(path); if (params.length === 0) return; // next statement can be expensive - ref.flat = - ref.flat || - flattenIO( - z.toJSONSchema(endpoint.inputSchema, { - unrepresentable: "any", - io: "input", - }), - ); + ref.flat ??= flattenIO( + z.toJSONSchema(endpoint.inputSchema, { + unrepresentable: "any", + io: "input", + }), + ); for (const param of params) { if (param in ref.flat.properties) continue; this.logger.warn( @@ -100,6 +98,6 @@ export class Diagnostics { this.#verified.set(endpoint, ref); } this.#checkSchema(ref, method, path, endpoint); - this.#checkPath(ref, method, path, endpoint); + this.#checkPathParams(ref, method, path, endpoint); } } From 797d7234c0d953d1935ea2234f71a16fb94129fc Mon Sep 17 00:00:00 2001 From: Anna Bocharova Date: Wed, 3 Dec 2025 08:28:52 +0100 Subject: [PATCH 09/13] Ref: DNRY ctx in checkSchema. --- express-zod-api/src/diagnostics.ts | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/express-zod-api/src/diagnostics.ts b/express-zod-api/src/diagnostics.ts index 5f89ad071a..5afcfc9086 100644 --- a/express-zod-api/src/diagnostics.ts +++ b/express-zod-api/src/diagnostics.ts @@ -21,9 +21,8 @@ export class Diagnostics { #checkSchema( ref: Cache, - method: Method, - path: string, endpoint: AbstractEndpoint, + ctx: { method: Method; path: string }, ): void { if (ref.hasValidSchema) return; for (const dir of ["input", "output"] as const) { @@ -32,12 +31,8 @@ export class Diagnostics { ]; while (stack.length > 0) { const entry = stack.shift()!; - if (entry.type && entry.type !== "object") { - this.logger.warn(`Endpoint ${dir} schema is not object-based`, { - method, - path, - }); - } + if (entry.type && entry.type !== "object") + this.logger.warn(`Endpoint ${dir} schema is not object-based`, ctx); for (const prop of ["allOf", "oneOf", "anyOf"] as const) if (entry[prop]) stack.push(...entry[prop]); } @@ -47,7 +42,7 @@ export class Diagnostics { if (reason) { this.logger.warn( "The final input schema of the endpoint contains an unsupported JSON payload type.", - { method, path, reason }, + { ...ctx, reason }, ); } } @@ -58,7 +53,7 @@ export class Diagnostics { if (reason) { this.logger.warn( `The final ${variant} response schema of the endpoint contains an unsupported JSON payload type.`, - { method, path, reason }, + { ...ctx, reason }, ); } } @@ -97,7 +92,7 @@ export class Diagnostics { ref = { hasValidSchema: false, paths: [] }; this.#verified.set(endpoint, ref); } - this.#checkSchema(ref, method, path, endpoint); + this.#checkSchema(ref, endpoint, { method, path }); this.#checkPathParams(ref, method, path, endpoint); } } From 354782d7ecd7eefa6cf35b7978ea023bc82fa0ef Mon Sep 17 00:00:00 2001 From: Anna Bocharova Date: Wed, 3 Dec 2025 09:42:23 +0100 Subject: [PATCH 10/13] Add similarity constraints over the OnEndpoint type. --- express-zod-api/src/diagnostics.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/express-zod-api/src/diagnostics.ts b/express-zod-api/src/diagnostics.ts index 5afcfc9086..2a742ff69d 100644 --- a/express-zod-api/src/diagnostics.ts +++ b/express-zod-api/src/diagnostics.ts @@ -7,6 +7,7 @@ import { AbstractEndpoint } from "./endpoint"; import { flattenIO } from "./json-schema-helpers"; import { ActualLogger } from "./logger-helpers"; import { Method } from "./method"; +import type { OnEndpoint } from "./routing-walker"; interface Cache { hasValidSchema: boolean; @@ -86,7 +87,7 @@ export class Diagnostics { ref.paths.push(path); } - public check(method: Method, path: string, endpoint: AbstractEndpoint): void { + public check: OnEndpoint = (method, path, endpoint) => { let ref = this.#verified.get(endpoint); if (!ref) { ref = { hasValidSchema: false, paths: [] }; @@ -94,5 +95,5 @@ export class Diagnostics { } this.#checkSchema(ref, endpoint, { method, path }); this.#checkPathParams(ref, method, path, endpoint); - } + }; } From b7ca6e2f969e99eaeb10009d31fac2a291b4fb15 Mon Sep 17 00:00:00 2001 From: Anna Bocharova Date: Thu, 4 Dec 2025 06:29:08 +0100 Subject: [PATCH 11/13] Using Set for paths. --- express-zod-api/src/diagnostics.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/express-zod-api/src/diagnostics.ts b/express-zod-api/src/diagnostics.ts index 2a742ff69d..f29da5294a 100644 --- a/express-zod-api/src/diagnostics.ts +++ b/express-zod-api/src/diagnostics.ts @@ -12,7 +12,7 @@ import type { OnEndpoint } from "./routing-walker"; interface Cache { hasValidSchema: boolean; flat?: ReturnType; - paths: string[]; + paths: Set; } export class Diagnostics { @@ -68,7 +68,7 @@ export class Diagnostics { path: string, endpoint: AbstractEndpoint, ): void { - if (ref.paths.includes(path)) return; + if (ref.paths.has(path)) return; const params = getRoutePathParams(path); if (params.length === 0) return; // next statement can be expensive ref.flat ??= flattenIO( @@ -84,13 +84,13 @@ export class Diagnostics { { method, path, param }, ); } - ref.paths.push(path); + ref.paths.add(path); } public check: OnEndpoint = (method, path, endpoint) => { let ref = this.#verified.get(endpoint); if (!ref) { - ref = { hasValidSchema: false, paths: [] }; + ref = { hasValidSchema: false, paths: new Set() }; this.#verified.set(endpoint, ref); } this.#checkSchema(ref, endpoint, { method, path }); From a8602252cafb11afce7e64c2d2d6000f3cdd5ae8 Mon Sep 17 00:00:00 2001 From: Anna Bocharova Date: Thu, 4 Dec 2025 07:57:34 +0100 Subject: [PATCH 12/13] Ref: proper naming. --- express-zod-api/src/diagnostics.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/express-zod-api/src/diagnostics.ts b/express-zod-api/src/diagnostics.ts index f29da5294a..183922dcb4 100644 --- a/express-zod-api/src/diagnostics.ts +++ b/express-zod-api/src/diagnostics.ts @@ -9,23 +9,23 @@ import { ActualLogger } from "./logger-helpers"; import { Method } from "./method"; import type { OnEndpoint } from "./routing-walker"; -interface Cache { - hasValidSchema: boolean; +interface Findings { + isSchemaChecked: boolean; flat?: ReturnType; paths: Set; } export class Diagnostics { - #verified = new WeakMap(); + #verified = new WeakMap(); constructor(protected logger: ActualLogger) {} #checkSchema( - ref: Cache, + ref: Findings, endpoint: AbstractEndpoint, ctx: { method: Method; path: string }, ): void { - if (ref.hasValidSchema) return; + if (ref.isSchemaChecked) return; for (const dir of ["input", "output"] as const) { const stack = [ z.toJSONSchema(endpoint[`${dir}Schema`], { unrepresentable: "any" }), @@ -59,11 +59,11 @@ export class Diagnostics { } } } - ref.hasValidSchema = true; + ref.isSchemaChecked = true; } #checkPathParams( - ref: Cache, + ref: Findings, method: Method, path: string, endpoint: AbstractEndpoint, @@ -90,7 +90,7 @@ export class Diagnostics { public check: OnEndpoint = (method, path, endpoint) => { let ref = this.#verified.get(endpoint); if (!ref) { - ref = { hasValidSchema: false, paths: new Set() }; + ref = { isSchemaChecked: false, paths: new Set() }; this.#verified.set(endpoint, ref); } this.#checkSchema(ref, endpoint, { method, path }); From 41a74a5642dd61594fa2311a1b66bd0546c470e2 Mon Sep 17 00:00:00 2001 From: Anna Bocharova Date: Thu, 4 Dec 2025 08:01:04 +0100 Subject: [PATCH 13/13] Ref: no import Method by restoring ctx approach. --- express-zod-api/src/diagnostics.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/express-zod-api/src/diagnostics.ts b/express-zod-api/src/diagnostics.ts index 183922dcb4..1c90ce96ca 100644 --- a/express-zod-api/src/diagnostics.ts +++ b/express-zod-api/src/diagnostics.ts @@ -1,12 +1,11 @@ import { z } from "zod"; import { responseVariants } from "./api-response"; -import { getRoutePathParams } from "./common-helpers"; +import { FlatObject, getRoutePathParams } from "./common-helpers"; import { contentTypes } from "./content-type"; import { findJsonIncompatible } from "./deep-checks"; import { AbstractEndpoint } from "./endpoint"; import { flattenIO } from "./json-schema-helpers"; import { ActualLogger } from "./logger-helpers"; -import { Method } from "./method"; import type { OnEndpoint } from "./routing-walker"; interface Findings { @@ -23,7 +22,7 @@ export class Diagnostics { #checkSchema( ref: Findings, endpoint: AbstractEndpoint, - ctx: { method: Method; path: string }, + ctx: FlatObject, ): void { if (ref.isSchemaChecked) return; for (const dir of ["input", "output"] as const) { @@ -64,9 +63,9 @@ export class Diagnostics { #checkPathParams( ref: Findings, - method: Method, - path: string, endpoint: AbstractEndpoint, + path: string, + ctx: FlatObject, ): void { if (ref.paths.has(path)) return; const params = getRoutePathParams(path); @@ -81,7 +80,7 @@ export class Diagnostics { if (param in ref.flat.properties) continue; this.logger.warn( "The input schema of the endpoint is most likely missing the parameter of the path it's assigned to.", - { method, path, param }, + { ...ctx, path, param }, ); } ref.paths.add(path); @@ -94,6 +93,6 @@ export class Diagnostics { this.#verified.set(endpoint, ref); } this.#checkSchema(ref, endpoint, { method, path }); - this.#checkPathParams(ref, method, path, endpoint); + this.#checkPathParams(ref, endpoint, path, { method }); }; }