From 25b4bedc2e324fcf639796f409107edf7419aa49 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Fri, 24 Apr 2026 10:44:15 +0200 Subject: [PATCH 01/43] feat(combinations): optional limit arg and implementation. --- express-zod-api/src/common-helpers.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/express-zod-api/src/common-helpers.ts b/express-zod-api/src/common-helpers.ts index d727e4cc04..0deaa21965 100644 --- a/express-zod-api/src/common-helpers.ts +++ b/express-zod-api/src/common-helpers.ts @@ -113,7 +113,16 @@ export const combinations = ( a: T[], b: T[], merge: (pair: [T, T]) => T, -): T[] => (a.length && b.length ? R.xprod(a, b).map(merge) : a.concat(b)); + limit = Infinity, +): T[] => { + if (!a.length || !b.length) return a.concat(b); + const result: T[] = []; + for (let idxA = 0; idxA < a.length && result.length < limit; idxA++) { + for (let idxB = 0; idxB < b.length && result.length < limit; idxB++) + result.push(merge([a[idxA], b[idxB]])); + } + return result; +}; export const ucFirst = (subject: string) => subject.charAt(0).toUpperCase() + subject.slice(1).toLowerCase(); From ac7d133e493cbf5dcb6b0570fbc0fde5109c6620 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Fri, 24 Apr 2026 11:13:40 +0200 Subject: [PATCH 02/43] rm redundant pair, add test for limit. --- express-zod-api/src/common-helpers.ts | 14 +++++++------- express-zod-api/src/json-schema-helpers.ts | 4 ++-- express-zod-api/src/logical-container.ts | 2 +- express-zod-api/src/result-helpers.ts | 2 +- express-zod-api/tests/common-helpers.spec.ts | 14 ++++++++++---- 5 files changed, 21 insertions(+), 15 deletions(-) diff --git a/express-zod-api/src/common-helpers.ts b/express-zod-api/src/common-helpers.ts index 0deaa21965..327d71595c 100644 --- a/express-zod-api/src/common-helpers.ts +++ b/express-zod-api/src/common-helpers.ts @@ -110,16 +110,16 @@ export const isSchema = ( (type ? R.path(["_zod", "def", "type"], subject) === type : true); export const combinations = ( - a: T[], - b: T[], - merge: (pair: [T, T]) => T, + left: T[], + right: T[], + merge: (a: T, b: T) => T, limit = Infinity, ): T[] => { - if (!a.length || !b.length) return a.concat(b); + if (!left.length || !right.length) return left.concat(right); const result: T[] = []; - for (let idxA = 0; idxA < a.length && result.length < limit; idxA++) { - for (let idxB = 0; idxB < b.length && result.length < limit; idxB++) - result.push(merge([a[idxA], b[idxB]])); + for (let idxL = 0; idxL < left.length && result.length < limit; idxL++) { + for (let idxR = 0; idxR < right.length && result.length < limit; idxR++) + result.push(merge(left[idxL], right[idxR])); } return result; }; diff --git a/express-zod-api/src/json-schema-helpers.ts b/express-zod-api/src/json-schema-helpers.ts index 3eceb01a1f..11851f0ba1 100644 --- a/express-zod-api/src/json-schema-helpers.ts +++ b/express-zod-api/src/json-schema-helpers.ts @@ -95,7 +95,7 @@ export const mergeExamples = ( target.examples = combinations( target.examples?.filter(isObject) || [], entry.examples.filter(isObject), - ([a, b]) => R.mergeDeepRight(a, b), + (a, b) => R.mergeDeepRight(a, b), ); } }; @@ -133,7 +133,7 @@ export const pullRequestExamples = (subject: z.core.JSONSchema.ObjectSchema) => Object.entries(subject.properties || {}).reduce( (acc, [key, prop]) => { const { examples = [] } = isObject(prop) ? prop : {}; - return combinations(acc, examples.map(R.objOf(key)), ([left, right]) => ({ + return combinations(acc, examples.map(R.objOf(key)), (left, right) => ({ ...left, ...right, })); diff --git a/express-zod-api/src/logical-container.ts b/express-zod-api/src/logical-container.ts index 5880113711..a922733176 100644 --- a/express-zod-api/src/logical-container.ts +++ b/express-zod-api/src/logical-container.ts @@ -36,7 +36,7 @@ export const processContainers = ( combinations( acc, R.map((opt) => (isSimple(opt) ? [opt] : opt.and), entry), - ([a, b]) => R.concat(a, b), + (a, b) => R.concat(a, b), ), R.reject(R.isEmpty, [persistent]), ); diff --git a/express-zod-api/src/result-helpers.ts b/express-zod-api/src/result-helpers.ts index 0f7cd11627..fb1992e3d3 100644 --- a/express-zod-api/src/result-helpers.ts +++ b/express-zod-api/src/result-helpers.ts @@ -92,7 +92,7 @@ export const pullResponseExamples = (subject: T) => Object.entries(subject._zod.def.shape).reduce( (acc, [key, schema]) => { const { examples = [] } = globalRegistry.get(schema) || {}; - return combinations(acc, examples.map(R.objOf(key)), ([left, right]) => ({ + return combinations(acc, examples.map(R.objOf(key)), (left, right) => ({ ...left, ...right, })); diff --git a/express-zod-api/tests/common-helpers.spec.ts b/express-zod-api/tests/common-helpers.spec.ts index 96b24cc2da..c9e16c9d3d 100644 --- a/express-zod-api/tests/common-helpers.spec.ts +++ b/express-zod-api/tests/common-helpers.spec.ts @@ -270,15 +270,21 @@ describe("Common Helpers", () => { describe("combinations()", () => { test("should run callback on each combination of items from two arrays", () => { - expect(combinations([1, 2], [4, 5, 6], ([a, b]) => a + b)).toEqual([ + expect(combinations([1, 2], [4, 5, 6], (a, b) => a + b)).toEqual([ 5, 6, 7, 6, 7, 8, ]); }); + test("should limit the number of combinations", () => { + expect(combinations([1, 2], [4, 5, 6], (a, b) => a + b, 4)).toEqual([ + 5, 6, 7, 6, + ]); + }); + test("should handle one or two arrays are empty", () => { - expect(combinations([], [4, 5, 6], ([a, b]) => a + b)).toEqual([4, 5, 6]); - expect(combinations([1, 2, 3], [], ([a, b]) => a + b)).toEqual([1, 2, 3]); - expect(combinations([], [], ([a, b]) => a + b)).toEqual([]); + expect(combinations([], [4, 5, 6], (a, b) => a + b)).toEqual([4, 5, 6]); + expect(combinations([1, 2, 3], [], (a, b) => a + b)).toEqual([1, 2, 3]); + expect(combinations([], [], (a, b) => a + b)).toEqual([]); }); }); From 120fa0d49bc26c91d3f0e8f8cb0bad77c59213e5 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Fri, 24 Apr 2026 11:32:00 +0200 Subject: [PATCH 03/43] jsdoc. --- express-zod-api/src/common-helpers.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/express-zod-api/src/common-helpers.ts b/express-zod-api/src/common-helpers.ts index 327d71595c..e75a50956d 100644 --- a/express-zod-api/src/common-helpers.ts +++ b/express-zod-api/src/common-helpers.ts @@ -109,13 +109,16 @@ export const isSchema = ( "_zod" in subject && (type ? R.path(["_zod", "def", "type"], subject) === type : true); +/** Configurable replacement for R.xprod() */ export const combinations = ( left: T[], right: T[], + /** @desc The function that combines elements */ merge: (a: T, b: T) => T, + /** @desc Maximum number of combinations to generate, only applies to non-empty arrays */ limit = Infinity, ): T[] => { - if (!left.length || !right.length) return left.concat(right); + if (!left.length || !right.length) return left.concat(right); // immutable one or another const result: T[] = []; for (let idxL = 0; idxL < left.length && result.length < limit; idxL++) { for (let idxR = 0; idxR < right.length && result.length < limit; idxR++) From 3972cb24d664b2e5a8d7a19d359d8e2d8ededec3 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Fri, 24 Apr 2026 11:34:51 +0200 Subject: [PATCH 04/43] test that limit does not apply to empty arrays case. --- express-zod-api/tests/common-helpers.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/express-zod-api/tests/common-helpers.spec.ts b/express-zod-api/tests/common-helpers.spec.ts index c9e16c9d3d..6c11c3eb29 100644 --- a/express-zod-api/tests/common-helpers.spec.ts +++ b/express-zod-api/tests/common-helpers.spec.ts @@ -283,7 +283,9 @@ describe("Common Helpers", () => { test("should handle one or two arrays are empty", () => { expect(combinations([], [4, 5, 6], (a, b) => a + b)).toEqual([4, 5, 6]); - expect(combinations([1, 2, 3], [], (a, b) => a + b)).toEqual([1, 2, 3]); + expect(combinations([1, 2, 3], [], (a, b) => a + b, 1)).toEqual([ + 1, 2, 3, + ]); expect(combinations([], [], (a, b) => a + b)).toEqual([]); }); }); From a0adbb92fdc8bca07fc29ff9e3087b202b646921 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Fri, 24 Apr 2026 11:51:36 +0200 Subject: [PATCH 05/43] rm wrapping fn in mergeExamples. --- express-zod-api/src/json-schema-helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/express-zod-api/src/json-schema-helpers.ts b/express-zod-api/src/json-schema-helpers.ts index 11851f0ba1..8fe54f4c6a 100644 --- a/express-zod-api/src/json-schema-helpers.ts +++ b/express-zod-api/src/json-schema-helpers.ts @@ -95,7 +95,7 @@ export const mergeExamples = ( target.examples = combinations( target.examples?.filter(isObject) || [], entry.examples.filter(isObject), - (a, b) => R.mergeDeepRight(a, b), + R.mergeDeepRight, ); } }; From 21f8b8b5fba5f9474d38e4fb07a5e8aada91900a Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Fri, 24 Apr 2026 12:24:26 +0200 Subject: [PATCH 06/43] rm fn wrapper in processContainers. --- express-zod-api/src/logical-container.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/express-zod-api/src/logical-container.ts b/express-zod-api/src/logical-container.ts index a922733176..042e79181a 100644 --- a/express-zod-api/src/logical-container.ts +++ b/express-zod-api/src/logical-container.ts @@ -33,10 +33,10 @@ export const processContainers = ( const alternators = R.map(R.prop("or"), R.concat(ors, orsInAnds)); // no chain! return alternators.reduce( (acc, entry) => - combinations( + combinations>( acc, R.map((opt) => (isSimple(opt) ? [opt] : opt.and), entry), - (a, b) => R.concat(a, b), + R.concat, ), R.reject(R.isEmpty, [persistent]), ); From 7e084950993d30f200e26d6a1a326d8bfbe12935 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Fri, 24 Apr 2026 12:28:42 +0200 Subject: [PATCH 07/43] fix: rm fn wrappers from pullResponseExamples and pullResponseExamples. --- express-zod-api/src/json-schema-helpers.ts | 5 +---- express-zod-api/src/result-helpers.ts | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/express-zod-api/src/json-schema-helpers.ts b/express-zod-api/src/json-schema-helpers.ts index 8fe54f4c6a..6d193161ab 100644 --- a/express-zod-api/src/json-schema-helpers.ts +++ b/express-zod-api/src/json-schema-helpers.ts @@ -133,10 +133,7 @@ export const pullRequestExamples = (subject: z.core.JSONSchema.ObjectSchema) => Object.entries(subject.properties || {}).reduce( (acc, [key, prop]) => { const { examples = [] } = isObject(prop) ? prop : {}; - return combinations(acc, examples.map(R.objOf(key)), (left, right) => ({ - ...left, - ...right, - })); + return combinations(acc, examples.map(R.objOf(key)), R.mergeRight); }, [], ); diff --git a/express-zod-api/src/result-helpers.ts b/express-zod-api/src/result-helpers.ts index fb1992e3d3..3d24bd9fb3 100644 --- a/express-zod-api/src/result-helpers.ts +++ b/express-zod-api/src/result-helpers.ts @@ -92,10 +92,7 @@ export const pullResponseExamples = (subject: T) => Object.entries(subject._zod.def.shape).reduce( (acc, [key, schema]) => { const { examples = [] } = globalRegistry.get(schema) || {}; - return combinations(acc, examples.map(R.objOf(key)), (left, right) => ({ - ...left, - ...right, - })); + return combinations(acc, examples.map(R.objOf(key)), R.mergeRight); }, [], ); From d07cefb20627f022ed9bc5780a2086b9995fef1a Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Fri, 24 Apr 2026 13:03:22 +0200 Subject: [PATCH 08/43] feat(processContainers): Add maxCombinations optional argument. --- express-zod-api/src/logical-container.ts | 2 ++ express-zod-api/tests/logical-container.spec.ts | 15 +++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/express-zod-api/src/logical-container.ts b/express-zod-api/src/logical-container.ts index 042e79181a..a851cc9146 100644 --- a/express-zod-api/src/logical-container.ts +++ b/express-zod-api/src/logical-container.ts @@ -24,6 +24,7 @@ export type Alternatives = Array>; export const processContainers = ( containers: LogicalContainer[], + maxCombinations = Infinity, ): Alternatives => { const simples = R.filter(isSimple, containers); const ands = R.chain(R.prop("and"), R.filter(isLogicalAnd, containers)); @@ -37,6 +38,7 @@ export const processContainers = ( acc, R.map((opt) => (isSimple(opt) ? [opt] : opt.and), entry), R.concat, + maxCombinations, ), R.reject(R.isEmpty, [persistent]), ); diff --git a/express-zod-api/tests/logical-container.spec.ts b/express-zod-api/tests/logical-container.spec.ts index c39b1e6027..b7612f77c6 100644 --- a/express-zod-api/tests/logical-container.spec.ts +++ b/express-zod-api/tests/logical-container.spec.ts @@ -123,5 +123,20 @@ describe("LogicalContainer", () => { processContainers([{ type: "bearer", format: "JWT" }, { and: [] }]), ).toEqual([[{ type: "bearer", format: "JWT" }]]); }); + + test("should control the maximum combinations", () => { + expect( + processContainers( + [ + { or: [{ and: [1, 2] }, { and: [3, 4] }] }, + { or: [{ and: [5, 6] }, { and: [7, 8] }] }, + ], + 2, + ), + ).toEqual([ + [1, 2, 5, 6], + [1, 2, 7, 8], + ]); + }); }); }); From 6cf244947c96d061d982e204858a792204048d2b Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Fri, 24 Apr 2026 13:06:20 +0200 Subject: [PATCH 09/43] feat: maxCombinations in CommonConfig with usage by Documentation in security container processing. --- express-zod-api/src/config-type.ts | 2 ++ express-zod-api/src/documentation.ts | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/express-zod-api/src/config-type.ts b/express-zod-api/src/config-type.ts index 7b7f781703..fb975cebde 100644 --- a/express-zod-api/src/config-type.ts +++ b/express-zod-api/src/config-type.ts @@ -89,6 +89,8 @@ export interface CommonConfig { * @see defaultInputSources */ inputSources?: Partial; + /** @default Infinity */ + maxCombinations?: number; } type BeforeUpload = (params: { diff --git a/express-zod-api/src/documentation.ts b/express-zod-api/src/documentation.ts index 173930cbe6..8326a0edd0 100644 --- a/express-zod-api/src/documentation.ts +++ b/express-zod-api/src/documentation.ts @@ -189,7 +189,10 @@ export class Documentation extends OpenApiBuilder { ); const request = depictRequest({ ...commons, schema: inputSchema }); - const security = processContainers(endpoint.security); + const security = processContainers( + endpoint.security, + config.maxCombinations, + ); const depictedParams = depictRequestParams({ ...commons, inputSources, From 9b058480eb0f3f87a0228787dace94a18f7e810e Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Fri, 24 Apr 2026 15:51:52 +0200 Subject: [PATCH 10/43] feat: limit option for pullResponseExamples. --- express-zod-api/src/result-helpers.ts | 7 +++++-- express-zod-api/tests/result-helpers.spec.ts | 13 +++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/express-zod-api/src/result-helpers.ts b/express-zod-api/src/result-helpers.ts index 3d24bd9fb3..0fb20f96a5 100644 --- a/express-zod-api/src/result-helpers.ts +++ b/express-zod-api/src/result-helpers.ts @@ -88,11 +88,14 @@ export const getPublicErrorMessage = (error: HttpError): string => : error.message; /** @see pullRequestExamples */ -export const pullResponseExamples = (subject: T) => +export const pullResponseExamples = ( + subject: T, + limit = Infinity, +) => Object.entries(subject._zod.def.shape).reduce( (acc, [key, schema]) => { const { examples = [] } = globalRegistry.get(schema) || {}; - return combinations(acc, examples.map(R.objOf(key)), R.mergeRight); + return combinations(acc, examples.map(R.objOf(key)), R.mergeRight, limit); }, [], ); diff --git a/express-zod-api/tests/result-helpers.spec.ts b/express-zod-api/tests/result-helpers.spec.ts index 50526c067d..0262ea691e 100644 --- a/express-zod-api/tests/result-helpers.spec.ts +++ b/express-zod-api/tests/result-helpers.spec.ts @@ -110,6 +110,19 @@ describe("Result helpers", () => { { a: "three", b: 2, c: false }, ]); }); + + test("respects the given limit", () => { + const schema = z.object({ + a: z.string().meta({ examples: ["one", "two", "three"] }), + b: z.number().meta({ examples: [1, 2] }), + c: z.boolean().meta({ examples: [false] }), + }); + expect(pullResponseExamples(schema, 3)).toEqual([ + { a: "one", b: 1, c: false }, + { a: "one", b: 2, c: false }, + { a: "two", b: 1, c: false }, + ]); + }); }); describe.each(["development", "production"])( From 1bef48a99149f1dc7e2907248a32fbc0f417f37c Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Fri, 24 Apr 2026 15:55:45 +0200 Subject: [PATCH 11/43] add limit option to Endpoint::ensureOutputExamples(). --- express-zod-api/src/endpoint.ts | 4 ++-- express-zod-api/src/result-helpers.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/express-zod-api/src/endpoint.ts b/express-zod-api/src/endpoint.ts index eaf2f4ce4e..c56726adac 100644 --- a/express-zod-api/src/endpoint.ts +++ b/express-zod-api/src/endpoint.ts @@ -90,10 +90,10 @@ export class Endpoint< readonly #def: ConstructorParameters>[0]; /** considered expensive operation, only required for generators */ - #ensureOutputExamples = R.once(() => { + #ensureOutputExamples = R.once((limit?: number) => { if (globalRegistry.get(this.#def.outputSchema)?.examples?.length) return; // examples on output schema, or pull up: if (!isSchema(this.#def.outputSchema, "object")) return; - const examples = pullResponseExamples(this.#def.outputSchema); + const examples = pullResponseExamples(this.#def.outputSchema, limit); if (!examples.length) return; const current = this.#def.outputSchema.meta(); globalRegistry diff --git a/express-zod-api/src/result-helpers.ts b/express-zod-api/src/result-helpers.ts index 0fb20f96a5..04406b2fe2 100644 --- a/express-zod-api/src/result-helpers.ts +++ b/express-zod-api/src/result-helpers.ts @@ -90,7 +90,7 @@ export const getPublicErrorMessage = (error: HttpError): string => /** @see pullRequestExamples */ export const pullResponseExamples = ( subject: T, - limit = Infinity, + limit?: number, ) => Object.entries(subject._zod.def.shape).reduce( (acc, [key, schema]) => { From 5acceb9fa0aa9e29f3d0252c897b24b0b25fc1e2 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Fri, 24 Apr 2026 16:05:45 +0200 Subject: [PATCH 12/43] Add config argument to Endpoint::getResponses(). --- express-zod-api/src/diagnostics.ts | 4 +++- express-zod-api/src/documentation.ts | 2 +- express-zod-api/src/endpoint.ts | 8 ++++++-- express-zod-api/src/integration.ts | 2 +- express-zod-api/tests/endpoint.spec.ts | 4 +++- 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/express-zod-api/src/diagnostics.ts b/express-zod-api/src/diagnostics.ts index c21b602fef..e2f47ab812 100644 --- a/express-zod-api/src/diagnostics.ts +++ b/express-zod-api/src/diagnostics.ts @@ -47,7 +47,9 @@ export class Diagnostics { } } for (const variant of responseVariants) { - for (const { mimeTypes, schema } of endpoint.getResponses(variant)) { + for (const { mimeTypes, schema } of endpoint.getResponses(variant, { + maxCombinations: 0, // not required for this check + })) { if (!mimeTypes?.includes(contentTypes.json)) continue; const reason = findJsonIncompatible(schema, "output"); if (reason) { diff --git a/express-zod-api/src/documentation.ts b/express-zod-api/src/documentation.ts index 8326a0edd0..13af1469f7 100644 --- a/express-zod-api/src/documentation.ts +++ b/express-zod-api/src/documentation.ts @@ -208,7 +208,7 @@ export class Documentation extends OpenApiBuilder { const responses: ResponsesObject = {}; for (const variant of responseVariants) { - const apiResponses = endpoint.getResponses(variant); + const apiResponses = endpoint.getResponses(variant, config); for (const { mimeTypes, schema, statusCodes } of apiResponses) { for (const statusCode of statusCodes) { responses[statusCode] = depictResponse({ diff --git a/express-zod-api/src/endpoint.ts b/express-zod-api/src/endpoint.ts index c56726adac..bfd5c19510 100644 --- a/express-zod-api/src/endpoint.ts +++ b/express-zod-api/src/endpoint.ts @@ -57,6 +57,7 @@ export abstract class AbstractEndpoint { /** @internal */ public abstract getResponses( variant: ResponseVariant, + config: Pick, ): ReadonlyArray; /** @internal */ public abstract getOperationId(method: ClientMethod): string | undefined; @@ -172,8 +173,11 @@ export class Endpoint< } /** @internal */ - public override getResponses(variant: ResponseVariant) { - if (variant === "positive") this.#ensureOutputExamples(); + public override getResponses( + variant: ResponseVariant, + { maxCombinations }: Pick, + ) { + if (variant === "positive") this.#ensureOutputExamples(maxCombinations); return Object.freeze( variant === "negative" ? this.#def.resultHandler.getNegativeResponse() diff --git a/express-zod-api/src/integration.ts b/express-zod-api/src/integration.ts index 46f50438cb..873f6c0d03 100644 --- a/express-zod-api/src/integration.ts +++ b/express-zod-api/src/integration.ts @@ -107,7 +107,7 @@ export class Integration extends IntegrationBase { this.#program.push(input); const dictionaries = responseVariants.reduce( (agg, responseVariant) => { - const responses = endpoint.getResponses(responseVariant); + const responses = endpoint.getResponses(responseVariant, config); const props = R.chain(([idx, { schema, mimeTypes, statusCodes }]) => { const hasContent = shouldHaveContent(method, mimeTypes); const variantType = this.api.makeType( diff --git a/express-zod-api/tests/endpoint.spec.ts b/express-zod-api/tests/endpoint.spec.ts index 83584bc4f8..b6d312f91e 100644 --- a/express-zod-api/tests/endpoint.spec.ts +++ b/express-zod-api/tests/endpoint.spec.ts @@ -282,7 +282,9 @@ describe("Endpoint", () => { output: z.object({ something: z.number() }), handler: vi.fn(), }); - const responses = endpoint.getResponses(variant); + const responses = endpoint.getResponses(variant, { + maxCombinations: 0, + }); expect(responses).toMatchSnapshot(); expect(() => (responses as any[]).push()).toThrowError(/read only/); }, From b68608297d9e1483c0ac86eb93f81a9be6312ac2 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Fri, 24 Apr 2026 16:09:02 +0200 Subject: [PATCH 13/43] add limit option to pullRequestExamples. --- express-zod-api/src/json-schema-helpers.ts | 7 +++++-- .../tests/json-schema-helpers.spec.ts | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/express-zod-api/src/json-schema-helpers.ts b/express-zod-api/src/json-schema-helpers.ts index 6d193161ab..798c46b918 100644 --- a/express-zod-api/src/json-schema-helpers.ts +++ b/express-zod-api/src/json-schema-helpers.ts @@ -129,11 +129,14 @@ export const flattenIO = ( }; /** @see pullResponseExamples */ -export const pullRequestExamples = (subject: z.core.JSONSchema.ObjectSchema) => +export const pullRequestExamples = ( + subject: z.core.JSONSchema.ObjectSchema, + limit?: number, +) => Object.entries(subject.properties || {}).reduce( (acc, [key, prop]) => { const { examples = [] } = isObject(prop) ? prop : {}; - return combinations(acc, examples.map(R.objOf(key)), R.mergeRight); + return combinations(acc, examples.map(R.objOf(key)), R.mergeRight, limit); }, [], ); diff --git a/express-zod-api/tests/json-schema-helpers.spec.ts b/express-zod-api/tests/json-schema-helpers.spec.ts index 8cb2f67ffa..2335df7624 100644 --- a/express-zod-api/tests/json-schema-helpers.spec.ts +++ b/express-zod-api/tests/json-schema-helpers.spec.ts @@ -281,6 +281,24 @@ describe("JSON Schema helpers", () => { { name: "jane", age: 30 }, ]); }); + + test("should respect the given limit", () => { + expect( + pullRequestExamples( + { + type: "object", + properties: { + name: { type: "string", examples: ["john", "jane"] }, + age: { type: "number", examples: [25, 30] }, + }, + }, + 2, + ), + ).toEqual([ + { name: "john", age: 25 }, + { name: "john", age: 30 }, + ]); + }); }); describe("flattenIO()", () => { From b55f73eceb3141fdf84847214e85012d6bb309f6 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Fri, 24 Apr 2026 16:41:46 +0200 Subject: [PATCH 14/43] Add maxCombinations option to flattenIO and OpenAPI context. --- express-zod-api/src/diagnostics.ts | 1 + express-zod-api/src/documentation-helpers.ts | 11 +++++++---- express-zod-api/src/documentation.ts | 1 + express-zod-api/src/json-schema-helpers.ts | 15 +++++++++++++-- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/express-zod-api/src/diagnostics.ts b/express-zod-api/src/diagnostics.ts index e2f47ab812..57e016b87c 100644 --- a/express-zod-api/src/diagnostics.ts +++ b/express-zod-api/src/diagnostics.ts @@ -77,6 +77,7 @@ export class Diagnostics { unrepresentable: "any", io: "input", }), + { maxCombinations: 0 }, // not required for this check ); for (const param of params) { if (param in ref.flat.properties) continue; diff --git a/express-zod-api/src/documentation-helpers.ts b/express-zod-api/src/documentation-helpers.ts index cf9b78c263..9a00481c23 100644 --- a/express-zod-api/src/documentation-helpers.ts +++ b/express-zod-api/src/documentation-helpers.ts @@ -55,6 +55,7 @@ interface ReqResCommons { ) => ReferenceObject; path: string; method: ClientMethod; + maxCombinations?: number; } export interface OpenAPIContext extends ReqResCommons { @@ -126,9 +127,9 @@ export const depictUnion: Depicter = ({ zodSchema, jsonSchema }) => { }; export const depictIntersection = R.tryCatch( - ({ jsonSchema }) => { + ({ jsonSchema }, { maxCombinations }) => { if (!jsonSchema.allOf) throw "no allOf"; - return flattenIO(jsonSchema, "throw"); + return flattenIO(jsonSchema, { mode: "throw", maxCombinations }); }, (_err, { jsonSchema }) => jsonSchema, ); @@ -282,6 +283,7 @@ export const depictRequestParams = ({ composition, isHeader, security, + maxCombinations, description = `${method.toUpperCase()} ${path} Parameter`, }: ReqResCommons & { composition: "inline" | "components"; @@ -291,7 +293,7 @@ export const depictRequestParams = ({ isHeader?: IsHeader; security?: Alternatives; }) => { - const flat = flattenIO(request); + const flat = flattenIO(request, { maxCombinations }); const pathParams = getRoutePathParams(path); const isQueryEnabled = inputSources.includes("query"); const areParamsEnabled = inputSources.includes("params"); @@ -609,6 +611,7 @@ export const depictBody = ({ makeRef, composition, paramNames, + maxCombinations, description = `${method.toUpperCase()} ${path} Request body`, }: ReqResCommons & { schema: IOSchema; @@ -635,7 +638,7 @@ export const depictBody = ({ examples: enumerateExamples( examples.length ? examples - : flattenIO(request) + : flattenIO(request, { maxCombinations }) .examples?.filter( (one): one is FlatObject => isObject(one) && !Array.isArray(one), ) diff --git a/express-zod-api/src/documentation.ts b/express-zod-api/src/documentation.ts index 13af1469f7..b981894ed8 100644 --- a/express-zod-api/src/documentation.ts +++ b/express-zod-api/src/documentation.ts @@ -173,6 +173,7 @@ export class Documentation extends OpenApiBuilder { endpoint, composition, brandHandling, + maxCombinations: config.maxCombinations, makeRef: this.#makeRef.bind(this), }; const { description, shortDescription, scopes, inputSchema } = endpoint; diff --git a/express-zod-api/src/json-schema-helpers.ts b/express-zod-api/src/json-schema-helpers.ts index 798c46b918..505683cdf8 100644 --- a/express-zod-api/src/json-schema-helpers.ts +++ b/express-zod-api/src/json-schema-helpers.ts @@ -2,6 +2,7 @@ import * as R from "ramda"; import { combinations, FlatObject, isObject } from "./common-helpers"; import type { z } from "zod"; import type { SchemaObject } from "openapi3-ts/oas31"; +import { CommonConfig } from "./config-type"; type MergeMode = "coerce" | "throw"; type FlattenObjectSchema = z.core.JSONSchema.ObjectSchema & @@ -96,13 +97,20 @@ export const mergeExamples = ( target.examples?.filter(isObject) || [], entry.examples.filter(isObject), R.mergeDeepRight, + // @todo: add limit ); } }; export const flattenIO = ( jsonSchema: z.core.JSONSchema.BaseSchema, - mode: MergeMode = "coerce", + { + mode = "coerce", + maxCombinations, + }: Pick & { + /** @default "coerce" */ + mode?: MergeMode; + } = {}, ) => { const stack: Stack = [R.pair(false, jsonSchema)]; // [isOptional, JSON Schema] const flat: FlattenObjectSchema = { type: "object", properties: {} }; @@ -114,7 +122,10 @@ export const flattenIO = ( stack.push(...processVariants(entry)); mergeExamples(flat, entry, isOptional); if (!isJsonObjectSchema(entry)) continue; - stack.push([isOptional, { examples: pullRequestExamples(entry) }]); + stack.push([ + isOptional, + { examples: pullRequestExamples(entry, maxCombinations) }, + ]); if (entry.properties) { flat.properties = (mode === "throw" ? propsMerger : R.mergeDeepRight)( flat.properties, From e95c5b5fb3e7da7a632e578fcbe99659a0398c51 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Fri, 24 Apr 2026 16:44:34 +0200 Subject: [PATCH 15/43] reusing maxCombinations by mergeExamples helper of flattenIO. --- express-zod-api/src/json-schema-helpers.ts | 9 ++++++--- .../tests/json-schema-helpers.spec.ts | 16 ++++++++++++---- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/express-zod-api/src/json-schema-helpers.ts b/express-zod-api/src/json-schema-helpers.ts index 505683cdf8..dcd1e9057f 100644 --- a/express-zod-api/src/json-schema-helpers.ts +++ b/express-zod-api/src/json-schema-helpers.ts @@ -87,7 +87,10 @@ export const processPropertyNames = ( export const mergeExamples = ( target: FlattenObjectSchema, entry: z.core.JSONSchema.BaseSchema, - isOptional: boolean, + { + isOptional, + maxCombinations, + }: Pick & { isOptional: boolean }, ) => { if (!entry.examples?.length) return; if (isOptional) { @@ -97,7 +100,7 @@ export const mergeExamples = ( target.examples?.filter(isObject) || [], entry.examples.filter(isObject), R.mergeDeepRight, - // @todo: add limit + maxCombinations, ); } }; @@ -120,7 +123,7 @@ export const flattenIO = ( if (entry.description) flat.description ??= entry.description; stack.push(...processAllOf(entry, mode, isOptional)); stack.push(...processVariants(entry)); - mergeExamples(flat, entry, isOptional); + mergeExamples(flat, entry, { isOptional, maxCombinations }); if (!isJsonObjectSchema(entry)) continue; stack.push([ isOptional, diff --git a/express-zod-api/tests/json-schema-helpers.spec.ts b/express-zod-api/tests/json-schema-helpers.spec.ts index 2335df7624..f39d6268a8 100644 --- a/express-zod-api/tests/json-schema-helpers.spec.ts +++ b/express-zod-api/tests/json-schema-helpers.spec.ts @@ -203,7 +203,7 @@ describe("JSON Schema helpers", () => { describe("mergeExamples()", () => { test("should do nothing when entry has no examples", () => { const flat = { type: "object" as const, properties: {} }; - mergeExamples(flat, { type: "string" }, false); + mergeExamples(flat, { type: "string" }, { isOptional: false }); expect(flat).toEqual({ type: "object", properties: {} }); }); @@ -213,7 +213,11 @@ describe("JSON Schema helpers", () => { properties: {}, examples: [{ a: 1 }], }; - mergeExamples(flat, { examples: [{ b: 2 }, { c: 3 }] }, true); + mergeExamples( + flat, + { examples: [{ b: 2 }, { c: 3 }] }, + { isOptional: true }, + ); expect(flat.examples).toEqual([{ a: 1 }, { b: 2 }, { c: 3 }]); }); @@ -221,7 +225,7 @@ describe("JSON Schema helpers", () => { "should initialize examples when flat has none (isOptional=%s)", (isOptional) => { const flat = { type: "object" as const, properties: {} }; - mergeExamples(flat, { examples: [{ a: 1 }] }, isOptional); + mergeExamples(flat, { examples: [{ a: 1 }] }, { isOptional }); expect(flat).toHaveProperty("examples", [{ a: 1 }]); }, ); @@ -232,7 +236,11 @@ describe("JSON Schema helpers", () => { properties: {}, examples: [{ a: 1 }], }; - mergeExamples(flat, { examples: [{ b: 2 }, { b: 3 }] }, false); + mergeExamples( + flat, + { examples: [{ b: 2 }, { b: 3 }] }, + { isOptional: false }, + ); expect(flat.examples).toEqual([ { a: 1, b: 2 }, { a: 1, b: 3 }, From f40c9a5c2af0390d8bb76a103e5d792d36c852dd Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Fri, 24 Apr 2026 17:01:11 +0200 Subject: [PATCH 16/43] fix: disabling combinations for Integration. --- express-zod-api/src/integration.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/express-zod-api/src/integration.ts b/express-zod-api/src/integration.ts index 873f6c0d03..e9b1599a11 100644 --- a/express-zod-api/src/integration.ts +++ b/express-zod-api/src/integration.ts @@ -107,7 +107,9 @@ export class Integration extends IntegrationBase { this.#program.push(input); const dictionaries = responseVariants.reduce( (agg, responseVariant) => { - const responses = endpoint.getResponses(responseVariant, config); + const responses = endpoint.getResponses(responseVariant, { + maxCombinations: 0, // not using examples yet + }); const props = R.chain(([idx, { schema, mimeTypes, statusCodes }]) => { const hasContent = shouldHaveContent(method, mimeTypes); const variantType = this.api.makeType( From 568ee1c3038382bd7f4377478e6be2a0bf9c2d9a Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Fri, 24 Apr 2026 17:15:05 +0200 Subject: [PATCH 17/43] fix: moving maxCombinations from CommonConfig to DocumentationParams. --- express-zod-api/src/config-type.ts | 2 -- express-zod-api/src/documentation.ts | 14 ++++++++------ express-zod-api/src/endpoint.ts | 4 ++-- express-zod-api/src/json-schema-helpers.ts | 6 +++--- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/express-zod-api/src/config-type.ts b/express-zod-api/src/config-type.ts index fb975cebde..7b7f781703 100644 --- a/express-zod-api/src/config-type.ts +++ b/express-zod-api/src/config-type.ts @@ -89,8 +89,6 @@ export interface CommonConfig { * @see defaultInputSources */ inputSources?: Partial; - /** @default Infinity */ - maxCombinations?: number; } type BeforeUpload = (params: { diff --git a/express-zod-api/src/documentation.ts b/express-zod-api/src/documentation.ts index b981894ed8..29c5986f89 100644 --- a/express-zod-api/src/documentation.ts +++ b/express-zod-api/src/documentation.ts @@ -86,6 +86,8 @@ interface DocumentationParams { * @example { users: "About users", files: { description: "About files", url: "https://example.com" } } * */ tags?: Parameters[0]; + /** @default Infinity */ + maxCombinations?: number; } export class Documentation extends OpenApiBuilder { @@ -161,6 +163,7 @@ export class Documentation extends OpenApiBuilder { hasSummaryFromDescription = true, hasHeadMethod = true, composition = "inline", + maxCombinations = Infinity, }: DocumentationParams) { super(); this.addInfo({ title, version }); @@ -173,7 +176,7 @@ export class Documentation extends OpenApiBuilder { endpoint, composition, brandHandling, - maxCombinations: config.maxCombinations, + maxCombinations, makeRef: this.#makeRef.bind(this), }; const { description, shortDescription, scopes, inputSchema } = endpoint; @@ -190,10 +193,7 @@ export class Documentation extends OpenApiBuilder { ); const request = depictRequest({ ...commons, schema: inputSchema }); - const security = processContainers( - endpoint.security, - config.maxCombinations, - ); + const security = processContainers(endpoint.security, maxCombinations); const depictedParams = depictRequestParams({ ...commons, inputSources, @@ -209,7 +209,9 @@ export class Documentation extends OpenApiBuilder { const responses: ResponsesObject = {}; for (const variant of responseVariants) { - const apiResponses = endpoint.getResponses(variant, config); + const apiResponses = endpoint.getResponses(variant, { + maxCombinations, + }); for (const { mimeTypes, schema, statusCodes } of apiResponses) { for (const statusCode of statusCodes) { responses[statusCode] = depictResponse({ diff --git a/express-zod-api/src/endpoint.ts b/express-zod-api/src/endpoint.ts index bfd5c19510..0b958a576b 100644 --- a/express-zod-api/src/endpoint.ts +++ b/express-zod-api/src/endpoint.ts @@ -57,7 +57,7 @@ export abstract class AbstractEndpoint { /** @internal */ public abstract getResponses( variant: ResponseVariant, - config: Pick, + params: { maxCombinations?: number }, ): ReadonlyArray; /** @internal */ public abstract getOperationId(method: ClientMethod): string | undefined; @@ -175,7 +175,7 @@ export class Endpoint< /** @internal */ public override getResponses( variant: ResponseVariant, - { maxCombinations }: Pick, + { maxCombinations }: { maxCombinations?: number }, ) { if (variant === "positive") this.#ensureOutputExamples(maxCombinations); return Object.freeze( diff --git a/express-zod-api/src/json-schema-helpers.ts b/express-zod-api/src/json-schema-helpers.ts index dcd1e9057f..98cdd5aa2b 100644 --- a/express-zod-api/src/json-schema-helpers.ts +++ b/express-zod-api/src/json-schema-helpers.ts @@ -2,7 +2,6 @@ import * as R from "ramda"; import { combinations, FlatObject, isObject } from "./common-helpers"; import type { z } from "zod"; import type { SchemaObject } from "openapi3-ts/oas31"; -import { CommonConfig } from "./config-type"; type MergeMode = "coerce" | "throw"; type FlattenObjectSchema = z.core.JSONSchema.ObjectSchema & @@ -90,7 +89,7 @@ export const mergeExamples = ( { isOptional, maxCombinations, - }: Pick & { isOptional: boolean }, + }: { isOptional: boolean; maxCombinations?: number }, ) => { if (!entry.examples?.length) return; if (isOptional) { @@ -110,9 +109,10 @@ export const flattenIO = ( { mode = "coerce", maxCombinations, - }: Pick & { + }: { /** @default "coerce" */ mode?: MergeMode; + maxCombinations?: number; } = {}, ) => { const stack: Stack = [R.pair(false, jsonSchema)]; // [isOptional, JSON Schema] From 81f1b764dfb0d11cc7e250966913c61abf94e1c4 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Fri, 24 Apr 2026 17:19:39 +0200 Subject: [PATCH 18/43] jsdoc. --- express-zod-api/src/documentation.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/express-zod-api/src/documentation.ts b/express-zod-api/src/documentation.ts index 29c5986f89..d489a4958f 100644 --- a/express-zod-api/src/documentation.ts +++ b/express-zod-api/src/documentation.ts @@ -86,7 +86,11 @@ interface DocumentationParams { * @example { users: "About users", files: { description: "About files", url: "https://example.com" } } * */ tags?: Parameters[0]; - /** @default Infinity */ + /** + * @desc Maximum number of combinations for examples and security access schemas + * @default Infinity + * @todo set to 20 in v28 + * */ maxCombinations?: number; } From ce3303df127c361b59eef98d8e7f2b13663c15a4 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Fri, 24 Apr 2026 18:56:22 +0200 Subject: [PATCH 19/43] add missing forwarding of maxCombinations into OpenAPIContext. --- express-zod-api/src/documentation-helpers.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/express-zod-api/src/documentation-helpers.ts b/express-zod-api/src/documentation-helpers.ts index 9a00481c23..4ae46a78e4 100644 --- a/express-zod-api/src/documentation-helpers.ts +++ b/express-zod-api/src/documentation-helpers.ts @@ -460,6 +460,7 @@ export const depictResponse = ({ hasMultipleStatusCodes, statusCode, brandHandling, + maxCombinations, description = `${method.toUpperCase()} ${path} ${ucFirst(variant)} response ${ hasMultipleStatusCodes ? statusCode : "" }`.trim(), @@ -477,7 +478,7 @@ export const depictResponse = ({ const response = asOAS( depict(schema, { rules: { ...brandHandling, ...depicters }, - ctx: { isResponse: true, makeRef, path, method }, + ctx: { isResponse: true, makeRef, path, method, maxCombinations }, }), ); const examples = []; @@ -593,13 +594,14 @@ export const depictRequest = ({ makeRef, path, method, + maxCombinations, }: ReqResCommons & { schema: IOSchema; brandHandling?: BrandHandling; }) => depict(schema, { rules: { ...brandHandling, ...depicters }, - ctx: { isResponse: false, makeRef, path, method }, + ctx: { isResponse: false, makeRef, path, method, maxCombinations }, }); export const depictBody = ({ From 337181506205b18640384f3c26328e06fac5cb9d Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Fri, 24 Apr 2026 19:01:56 +0200 Subject: [PATCH 20/43] fix: rm R.once() from Endpoint::ensureOutputExamples since it's already protected by registry check. --- express-zod-api/src/endpoint.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/express-zod-api/src/endpoint.ts b/express-zod-api/src/endpoint.ts index 0b958a576b..9b59721d52 100644 --- a/express-zod-api/src/endpoint.ts +++ b/express-zod-api/src/endpoint.ts @@ -91,7 +91,7 @@ export class Endpoint< readonly #def: ConstructorParameters>[0]; /** considered expensive operation, only required for generators */ - #ensureOutputExamples = R.once((limit?: number) => { + #ensureOutputExamples(limit?: number) { if (globalRegistry.get(this.#def.outputSchema)?.examples?.length) return; // examples on output schema, or pull up: if (!isSchema(this.#def.outputSchema, "object")) return; const examples = pullResponseExamples(this.#def.outputSchema, limit); @@ -100,7 +100,7 @@ export class Endpoint< globalRegistry .remove(this.#def.outputSchema) // reassign to avoid cloning .add(this.#def.outputSchema, { ...current, examples }); - }); + } constructor(def: { deprecated?: boolean; From 35a75f14abd6908e81a0f205e1ca2ef2f5b9e2b8 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Fri, 24 Apr 2026 19:31:39 +0200 Subject: [PATCH 21/43] fix: applying the limit to single arrays and handling edge cases. --- express-zod-api/src/common-helpers.ts | 5 +++-- express-zod-api/tests/common-helpers.spec.ts | 15 +++++++++------ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/express-zod-api/src/common-helpers.ts b/express-zod-api/src/common-helpers.ts index e75a50956d..29ee43ad92 100644 --- a/express-zod-api/src/common-helpers.ts +++ b/express-zod-api/src/common-helpers.ts @@ -115,10 +115,11 @@ export const combinations = ( right: T[], /** @desc The function that combines elements */ merge: (a: T, b: T) => T, - /** @desc Maximum number of combinations to generate, only applies to non-empty arrays */ + /** @desc Maximum number of combinations */ limit = Infinity, ): T[] => { - if (!left.length || !right.length) return left.concat(right); // immutable one or another + if (Number.isNaN(limit) || limit <= 0) return []; + if (!left.length || !right.length) return left.concat(right).slice(0, limit); const result: T[] = []; for (let idxL = 0; idxL < left.length && result.length < limit; idxL++) { for (let idxR = 0; idxR < right.length && result.length < limit; idxR++) diff --git a/express-zod-api/tests/common-helpers.spec.ts b/express-zod-api/tests/common-helpers.spec.ts index 6c11c3eb29..89ddb78550 100644 --- a/express-zod-api/tests/common-helpers.spec.ts +++ b/express-zod-api/tests/common-helpers.spec.ts @@ -275,18 +275,21 @@ describe("Common Helpers", () => { ]); }); + test("should handle one or two arrays are empty", () => { + expect(combinations([], [4, 5, 6], (a, b) => a + b)).toEqual([4, 5, 6]); + expect(combinations([1, 2, 3], [], (a, b) => a + b)).toEqual([1, 2, 3]); + expect(combinations([], [], (a, b) => a + b)).toEqual([]); + }); + test("should limit the number of combinations", () => { expect(combinations([1, 2], [4, 5, 6], (a, b) => a + b, 4)).toEqual([ 5, 6, 7, 6, ]); + expect(combinations([1, 2, 3], [], (a, b) => a + b, 1)).toEqual([1]); }); - test("should handle one or two arrays are empty", () => { - expect(combinations([], [4, 5, 6], (a, b) => a + b)).toEqual([4, 5, 6]); - expect(combinations([1, 2, 3], [], (a, b) => a + b, 1)).toEqual([ - 1, 2, 3, - ]); - expect(combinations([], [], (a, b) => a + b)).toEqual([]); + test.each([0, -1, NaN])("should return empty for limit=%s", (limit) => { + expect(combinations([1, 2], [3, 4], (a, b) => a + b, limit)).toEqual([]); }); }); From 876e45dffa951ee550fd742b8f0a6642137d0635 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Fri, 24 Apr 2026 19:39:26 +0200 Subject: [PATCH 22/43] Add jsdoc example for maxCombinations in Documentation. --- express-zod-api/src/documentation.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/express-zod-api/src/documentation.ts b/express-zod-api/src/documentation.ts index d489a4958f..d0fb7ec089 100644 --- a/express-zod-api/src/documentation.ts +++ b/express-zod-api/src/documentation.ts @@ -87,7 +87,8 @@ interface DocumentationParams { * */ tags?: Parameters[0]; /** - * @desc Maximum number of combinations for examples and security access schemas + * @desc Maximum number of combined examples and security schemes + * @example 0 — disables combining of examples and security schemes * @default Infinity * @todo set to 20 in v28 * */ From 5b0ae8dd6524a07b260ee5071e9949df95b40bbf Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Fri, 24 Apr 2026 19:46:00 +0200 Subject: [PATCH 23/43] jsdoc clarification for combinations(). --- express-zod-api/src/common-helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/express-zod-api/src/common-helpers.ts b/express-zod-api/src/common-helpers.ts index 29ee43ad92..ac8dea3084 100644 --- a/express-zod-api/src/common-helpers.ts +++ b/express-zod-api/src/common-helpers.ts @@ -109,7 +109,7 @@ export const isSchema = ( "_zod" in subject && (type ? R.path(["_zod", "def", "type"], subject) === type : true); -/** Configurable replacement for R.xprod() */ +/** Configurable replacement for R.xprod(), but it also handles empty arrays */ export const combinations = ( left: T[], right: T[], From 8d305ad09d936c5303fe6c057df030910e29db5d Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Fri, 24 Apr 2026 20:07:38 +0200 Subject: [PATCH 24/43] fix(diagnostics): readability. --- 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 57e016b87c..a134f7c68d 100644 --- a/express-zod-api/src/diagnostics.ts +++ b/express-zod-api/src/diagnostics.ts @@ -47,9 +47,8 @@ export class Diagnostics { } } for (const variant of responseVariants) { - for (const { mimeTypes, schema } of endpoint.getResponses(variant, { - maxCombinations: 0, // not required for this check - })) { + const responses = endpoint.getResponses(variant, { maxCombinations: 0 }); // no examples + for (const { mimeTypes, schema } of responses) { if (!mimeTypes?.includes(contentTypes.json)) continue; const reason = findJsonIncompatible(schema, "output"); if (reason) { From aa645d96080ca40172ed05f85f8a23f4849e828b Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Fri, 24 Apr 2026 20:13:31 +0200 Subject: [PATCH 25/43] fix(flattenIO): renaming MergeMode to isStrict. --- express-zod-api/src/documentation-helpers.ts | 2 +- express-zod-api/src/json-schema-helpers.ts | 15 +++++++-------- express-zod-api/tests/json-schema-helpers.spec.ts | 14 +++++++------- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/express-zod-api/src/documentation-helpers.ts b/express-zod-api/src/documentation-helpers.ts index 4ae46a78e4..c75b2d0fbe 100644 --- a/express-zod-api/src/documentation-helpers.ts +++ b/express-zod-api/src/documentation-helpers.ts @@ -129,7 +129,7 @@ export const depictUnion: Depicter = ({ zodSchema, jsonSchema }) => { export const depictIntersection = R.tryCatch( ({ jsonSchema }, { maxCombinations }) => { if (!jsonSchema.allOf) throw "no allOf"; - return flattenIO(jsonSchema, { mode: "throw", maxCombinations }); + return flattenIO(jsonSchema, { isStrict: true, maxCombinations }); }, (_err, { jsonSchema }) => jsonSchema, ); diff --git a/express-zod-api/src/json-schema-helpers.ts b/express-zod-api/src/json-schema-helpers.ts index 98cdd5aa2b..fac8a5738a 100644 --- a/express-zod-api/src/json-schema-helpers.ts +++ b/express-zod-api/src/json-schema-helpers.ts @@ -3,7 +3,6 @@ import { combinations, FlatObject, isObject } from "./common-helpers"; import type { z } from "zod"; import type { SchemaObject } from "openapi3-ts/oas31"; -type MergeMode = "coerce" | "throw"; type FlattenObjectSchema = z.core.JSONSchema.ObjectSchema & Required>; @@ -42,12 +41,12 @@ type Stack = Array>; /** @internal */ export const processAllOf = ( subject: z.core.JSONSchema.BaseSchema, - mode: MergeMode, + isStrict: boolean, isOptional: boolean, ) => { if (!("allOf" in subject) || !subject.allOf) return []; return subject.allOf.map((one) => { - if (mode === "throw" && !(one.type === "object" && canMerge(one))) + if (isStrict && !(one.type === "object" && canMerge(one))) throw new Error("Can not merge"); return R.pair(isOptional, one); }); @@ -107,11 +106,11 @@ export const mergeExamples = ( export const flattenIO = ( jsonSchema: z.core.JSONSchema.BaseSchema, { - mode = "coerce", + isStrict = false, maxCombinations, }: { - /** @default "coerce" */ - mode?: MergeMode; + /** @default false */ + isStrict?: boolean; maxCombinations?: number; } = {}, ) => { @@ -121,7 +120,7 @@ export const flattenIO = ( for (let idx = 0; idx < stack.length; idx++) { const [isOptional, entry] = stack[idx]; if (entry.description) flat.description ??= entry.description; - stack.push(...processAllOf(entry, mode, isOptional)); + stack.push(...processAllOf(entry, isStrict, isOptional)); stack.push(...processVariants(entry)); mergeExamples(flat, entry, { isOptional, maxCombinations }); if (!isJsonObjectSchema(entry)) continue; @@ -130,7 +129,7 @@ export const flattenIO = ( { examples: pullRequestExamples(entry, maxCombinations) }, ]); if (entry.properties) { - flat.properties = (mode === "throw" ? propsMerger : R.mergeDeepRight)( + flat.properties = (isStrict ? propsMerger : R.mergeDeepRight)( flat.properties, entry.properties, ); diff --git a/express-zod-api/tests/json-schema-helpers.spec.ts b/express-zod-api/tests/json-schema-helpers.spec.ts index f39d6268a8..2e0bde6375 100644 --- a/express-zod-api/tests/json-schema-helpers.spec.ts +++ b/express-zod-api/tests/json-schema-helpers.spec.ts @@ -70,19 +70,19 @@ describe("JSON Schema helpers", () => { test("should return empty array when no allOf", () => { const result = processAllOf( { type: "object", properties: {} }, - "coerce", + false, false, ); expect(result).toEqual([]); }); - test("should map allOf entries with isOptional flag in coerce mode", () => { + test("should map allOf entries with isOptional flag in non-strict mode", () => { const result = processAllOf( { type: "object", allOf: [{ type: "object", properties: { a: { type: "string" } } }], }, - "coerce", + false, true, ); expect(result).toEqual([ @@ -90,23 +90,23 @@ describe("JSON Schema helpers", () => { ]); }); - test("should throw in throw mode when schema cannot be merged", () => { + test("should throw in strict mode when schema cannot be merged", () => { expect(() => processAllOf( { type: "object", allOf: [{ type: "string" }] }, - "throw", + true, false, ), ).toThrow("Can not merge"); }); - test("should allow mergeable schemas in throw mode", () => { + test("should allow mergeable schemas in strict mode", () => { const result = processAllOf( { type: "object", allOf: [{ type: "object", properties: { a: { type: "string" } } }], }, - "throw", + true, false, ); expect(result).toEqual([ From 95277631a74749bab56b1218e60ca03632163876 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Fri, 24 Apr 2026 20:17:18 +0200 Subject: [PATCH 26/43] fix(processAllOf): named options. --- express-zod-api/src/json-schema-helpers.ts | 5 ++--- express-zod-api/tests/json-schema-helpers.spec.ts | 12 ++++-------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/express-zod-api/src/json-schema-helpers.ts b/express-zod-api/src/json-schema-helpers.ts index fac8a5738a..3ceaea9903 100644 --- a/express-zod-api/src/json-schema-helpers.ts +++ b/express-zod-api/src/json-schema-helpers.ts @@ -41,8 +41,7 @@ type Stack = Array>; /** @internal */ export const processAllOf = ( subject: z.core.JSONSchema.BaseSchema, - isStrict: boolean, - isOptional: boolean, + { isStrict, isOptional }: { isStrict: boolean; isOptional: boolean }, ) => { if (!("allOf" in subject) || !subject.allOf) return []; return subject.allOf.map((one) => { @@ -120,7 +119,7 @@ export const flattenIO = ( for (let idx = 0; idx < stack.length; idx++) { const [isOptional, entry] = stack[idx]; if (entry.description) flat.description ??= entry.description; - stack.push(...processAllOf(entry, isStrict, isOptional)); + stack.push(...processAllOf(entry, { isStrict, isOptional })); stack.push(...processVariants(entry)); mergeExamples(flat, entry, { isOptional, maxCombinations }); if (!isJsonObjectSchema(entry)) continue; diff --git a/express-zod-api/tests/json-schema-helpers.spec.ts b/express-zod-api/tests/json-schema-helpers.spec.ts index 2e0bde6375..8564cec215 100644 --- a/express-zod-api/tests/json-schema-helpers.spec.ts +++ b/express-zod-api/tests/json-schema-helpers.spec.ts @@ -70,8 +70,7 @@ describe("JSON Schema helpers", () => { test("should return empty array when no allOf", () => { const result = processAllOf( { type: "object", properties: {} }, - false, - false, + { isStrict: false, isOptional: false }, ); expect(result).toEqual([]); }); @@ -82,8 +81,7 @@ describe("JSON Schema helpers", () => { type: "object", allOf: [{ type: "object", properties: { a: { type: "string" } } }], }, - false, - true, + { isStrict: false, isOptional: true }, ); expect(result).toEqual([ [true, { type: "object", properties: { a: { type: "string" } } }], @@ -94,8 +92,7 @@ describe("JSON Schema helpers", () => { expect(() => processAllOf( { type: "object", allOf: [{ type: "string" }] }, - true, - false, + { isStrict: true, isOptional: false }, ), ).toThrow("Can not merge"); }); @@ -106,8 +103,7 @@ describe("JSON Schema helpers", () => { type: "object", allOf: [{ type: "object", properties: { a: { type: "string" } } }], }, - true, - false, + { isStrict: true, isOptional: false }, ); expect(result).toEqual([ [false, { type: "object", properties: { a: { type: "string" } } }], From 60ee257343f4550c8b9e65d3962e3cf188321e69 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Fri, 24 Apr 2026 21:08:42 +0200 Subject: [PATCH 27/43] fix(processContainers): early exit for zero limit. --- express-zod-api/src/logical-container.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/express-zod-api/src/logical-container.ts b/express-zod-api/src/logical-container.ts index a851cc9146..0d49da1172 100644 --- a/express-zod-api/src/logical-container.ts +++ b/express-zod-api/src/logical-container.ts @@ -26,6 +26,7 @@ export const processContainers = ( containers: LogicalContainer[], maxCombinations = Infinity, ): Alternatives => { + if (!(maxCombinations > 0)) return []; const simples = R.filter(isSimple, containers); const ands = R.chain(R.prop("and"), R.filter(isLogicalAnd, containers)); const [simpleAnds, orsInAnds] = R.partition(isSimple, ands); From 60cbf12b29939851f91a1229619f0f14999895c1 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Fri, 24 Apr 2026 21:09:06 +0200 Subject: [PATCH 28/43] fix(combinations): shorter condition for NaN. --- express-zod-api/src/common-helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/express-zod-api/src/common-helpers.ts b/express-zod-api/src/common-helpers.ts index ac8dea3084..18c659a59f 100644 --- a/express-zod-api/src/common-helpers.ts +++ b/express-zod-api/src/common-helpers.ts @@ -118,7 +118,7 @@ export const combinations = ( /** @desc Maximum number of combinations */ limit = Infinity, ): T[] => { - if (Number.isNaN(limit) || limit <= 0) return []; + if (!(limit > 0)) return []; if (!left.length || !right.length) return left.concat(right).slice(0, limit); const result: T[] = []; for (let idxL = 0; idxL < left.length && result.length < limit; idxL++) { From 87aea9726979beec943216b6046369e529bed652 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Fri, 24 Apr 2026 21:36:21 +0200 Subject: [PATCH 29/43] fix(mergeExamples): simmetrical limit and default value. --- express-zod-api/src/json-schema-helpers.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/express-zod-api/src/json-schema-helpers.ts b/express-zod-api/src/json-schema-helpers.ts index 3ceaea9903..275d430757 100644 --- a/express-zod-api/src/json-schema-helpers.ts +++ b/express-zod-api/src/json-schema-helpers.ts @@ -86,12 +86,15 @@ export const mergeExamples = ( entry: z.core.JSONSchema.BaseSchema, { isOptional, - maxCombinations, + maxCombinations = Infinity, }: { isOptional: boolean; maxCombinations?: number }, ) => { if (!entry.examples?.length) return; if (isOptional) { - target.examples = R.concat(target.examples || [], entry.examples); + target.examples = R.concat(target.examples || [], entry.examples).slice( + 0, + maxCombinations, + ); } else { target.examples = combinations( target.examples?.filter(isObject) || [], From 497ad4f2efe486f320db4180494a6e8a799c08dc Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Fri, 24 Apr 2026 21:37:39 +0200 Subject: [PATCH 30/43] fix(mergeExamples): early exit for zero limit. --- express-zod-api/src/json-schema-helpers.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/express-zod-api/src/json-schema-helpers.ts b/express-zod-api/src/json-schema-helpers.ts index 275d430757..eaa31cc615 100644 --- a/express-zod-api/src/json-schema-helpers.ts +++ b/express-zod-api/src/json-schema-helpers.ts @@ -89,6 +89,7 @@ export const mergeExamples = ( maxCombinations = Infinity, }: { isOptional: boolean; maxCombinations?: number }, ) => { + if (!(maxCombinations! > 0)) return; if (!entry.examples?.length) return; if (isOptional) { target.examples = R.concat(target.examples || [], entry.examples).slice( From 951a11d27f20bdaba4b726c7b89e749475f873ac Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Fri, 24 Apr 2026 21:45:10 +0200 Subject: [PATCH 31/43] tests for edge cases of processContainers and mergeExamples. --- .../tests/json-schema-helpers.spec.ts | 13 +++++++++ .../tests/logical-container.spec.ts | 28 +++++++++---------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/express-zod-api/tests/json-schema-helpers.spec.ts b/express-zod-api/tests/json-schema-helpers.spec.ts index 8564cec215..9304dba972 100644 --- a/express-zod-api/tests/json-schema-helpers.spec.ts +++ b/express-zod-api/tests/json-schema-helpers.spec.ts @@ -203,6 +203,19 @@ describe("JSON Schema helpers", () => { expect(flat).toEqual({ type: "object", properties: {} }); }); + test.each([0, -1, NaN])( + "should do nothing when maxCombinations=%s", + (maxCombinations) => { + const flat = { type: "object" as const, properties: {} }; + mergeExamples( + flat, + { examples: [{ a: 1 }] }, + { isOptional: false, maxCombinations }, + ); + expect(flat).toEqual({ type: "object", properties: {} }); + }, + ); + test("should concatenate examples when optional", () => { const flat = { type: "object" as const, diff --git a/express-zod-api/tests/logical-container.spec.ts b/express-zod-api/tests/logical-container.spec.ts index b7612f77c6..357a67d436 100644 --- a/express-zod-api/tests/logical-container.spec.ts +++ b/express-zod-api/tests/logical-container.spec.ts @@ -124,19 +124,19 @@ describe("LogicalContainer", () => { ).toEqual([[{ type: "bearer", format: "JWT" }]]); }); - test("should control the maximum combinations", () => { - expect( - processContainers( - [ - { or: [{ and: [1, 2] }, { and: [3, 4] }] }, - { or: [{ and: [5, 6] }, { and: [7, 8] }] }, - ], - 2, - ), - ).toEqual([ - [1, 2, 5, 6], - [1, 2, 7, 8], - ]); - }); + test.each([2, 0, -1, NaN])( + "should control the maximum combinations %s", + (maxCombinations) => { + expect( + processContainers( + [ + { or: [{ and: [1, 2] }, { and: [3, 4] }] }, + { or: [{ and: [5, 6] }, { and: [7, 8] }] }, + ], + maxCombinations, + ), + ).toHaveLength(Math.max(0, maxCombinations || 0)); + }, + ); }); }); From 3a02b85f7bb2717978242888db6ffe1b38f0c2f7 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Fri, 24 Apr 2026 21:46:46 +0200 Subject: [PATCH 32/43] fix: rm redundant coercion. --- express-zod-api/src/json-schema-helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/express-zod-api/src/json-schema-helpers.ts b/express-zod-api/src/json-schema-helpers.ts index eaa31cc615..4743fa3123 100644 --- a/express-zod-api/src/json-schema-helpers.ts +++ b/express-zod-api/src/json-schema-helpers.ts @@ -89,7 +89,7 @@ export const mergeExamples = ( maxCombinations = Infinity, }: { isOptional: boolean; maxCombinations?: number }, ) => { - if (!(maxCombinations! > 0)) return; + if (!(maxCombinations > 0)) return; if (!entry.examples?.length) return; if (isOptional) { target.examples = R.concat(target.examples || [], entry.examples).slice( From 3ff885f20305a7ab3626642559283b7f8f593d57 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Sat, 25 Apr 2026 06:08:27 +0200 Subject: [PATCH 33/43] Early exits for pullRequestExamples and pullResponseExamples. --- express-zod-api/src/json-schema-helpers.ts | 8 +++++--- express-zod-api/src/result-helpers.ts | 8 +++++--- express-zod-api/tests/json-schema-helpers.spec.ts | 14 ++++++++++++++ express-zod-api/tests/result-helpers.spec.ts | 7 +++++++ 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/express-zod-api/src/json-schema-helpers.ts b/express-zod-api/src/json-schema-helpers.ts index 4743fa3123..bde62c468a 100644 --- a/express-zod-api/src/json-schema-helpers.ts +++ b/express-zod-api/src/json-schema-helpers.ts @@ -147,12 +147,14 @@ export const flattenIO = ( /** @see pullResponseExamples */ export const pullRequestExamples = ( subject: z.core.JSONSchema.ObjectSchema, - limit?: number, -) => - Object.entries(subject.properties || {}).reduce( + limit = Infinity, +) => { + if (!(limit > 0)) return []; + return Object.entries(subject.properties || {}).reduce( (acc, [key, prop]) => { const { examples = [] } = isObject(prop) ? prop : {}; return combinations(acc, examples.map(R.objOf(key)), R.mergeRight, limit); }, [], ); +}; diff --git a/express-zod-api/src/result-helpers.ts b/express-zod-api/src/result-helpers.ts index 04406b2fe2..25ca6270a3 100644 --- a/express-zod-api/src/result-helpers.ts +++ b/express-zod-api/src/result-helpers.ts @@ -90,12 +90,14 @@ export const getPublicErrorMessage = (error: HttpError): string => /** @see pullRequestExamples */ export const pullResponseExamples = ( subject: T, - limit?: number, -) => - Object.entries(subject._zod.def.shape).reduce( + limit = Infinity, +) => { + if (!(limit > 0)) return []; + return Object.entries(subject._zod.def.shape).reduce( (acc, [key, schema]) => { const { examples = [] } = globalRegistry.get(schema) || {}; return combinations(acc, examples.map(R.objOf(key)), R.mergeRight, limit); }, [], ); +}; diff --git a/express-zod-api/tests/json-schema-helpers.spec.ts b/express-zod-api/tests/json-schema-helpers.spec.ts index 9304dba972..10c2d53436 100644 --- a/express-zod-api/tests/json-schema-helpers.spec.ts +++ b/express-zod-api/tests/json-schema-helpers.spec.ts @@ -316,6 +316,20 @@ describe("JSON Schema helpers", () => { { name: "john", age: 30 }, ]); }); + + test.each([0, -1, NaN])("should return empty for limit=%s", (limit) => { + expect( + pullRequestExamples( + { + type: "object", + properties: { + name: { type: "string", examples: ["john"] }, + }, + }, + limit, + ), + ).toEqual([]); + }); }); describe("flattenIO()", () => { diff --git a/express-zod-api/tests/result-helpers.spec.ts b/express-zod-api/tests/result-helpers.spec.ts index 0262ea691e..4d1074d2a9 100644 --- a/express-zod-api/tests/result-helpers.spec.ts +++ b/express-zod-api/tests/result-helpers.spec.ts @@ -123,6 +123,13 @@ describe("Result helpers", () => { { a: "two", b: 1, c: false }, ]); }); + + test.each([0, -1, NaN])("returns empty for limit=%s", (limit) => { + const schema = z.object({ + a: z.string().example("one"), + }); + expect(pullResponseExamples(schema, limit)).toEqual([]); + }); }); describe.each(["development", "production"])( From f70c318a478ed030239947413ce526dbb92ff135 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Sat, 25 Apr 2026 09:49:29 +0200 Subject: [PATCH 34/43] REF: take headers from endpoint.security instead of security scheme combinations. --- express-zod-api/src/documentation-helpers.ts | 12 ++++-------- express-zod-api/src/documentation.ts | 11 +++++++---- express-zod-api/src/logical-container.ts | 17 +++++++++++++++++ .../tests/documentation-helpers.spec.ts | 4 ++-- 4 files changed, 30 insertions(+), 14 deletions(-) diff --git a/express-zod-api/src/documentation-helpers.ts b/express-zod-api/src/documentation-helpers.ts index c75b2d0fbe..23f50b40ff 100644 --- a/express-zod-api/src/documentation-helpers.ts +++ b/express-zod-api/src/documentation-helpers.ts @@ -268,9 +268,9 @@ const enumerateExamples = (examples: unknown[]): ExamplesObject | undefined => export const defaultIsHeader = ( name: string, - familiar?: string[], + familiar?: Set, ): name is `x-${string}` => - familiar?.includes(name) || + familiar?.has(name) || name.startsWith("x-") || wellKnownHeaders.includes(name); @@ -282,7 +282,7 @@ export const depictRequestParams = ({ makeRef, composition, isHeader, - security, + securityHeaders, maxCombinations, description = `${method.toUpperCase()} ${path} Parameter`, }: ReqResCommons & { @@ -291,17 +291,13 @@ export const depictRequestParams = ({ request: z.core.JSONSchema.BaseSchema; inputSources: InputSource[]; isHeader?: IsHeader; - security?: Alternatives; + securityHeaders?: Set; }) => { const flat = flattenIO(request, { maxCombinations }); const pathParams = getRoutePathParams(path); const isQueryEnabled = inputSources.includes("query"); const areParamsEnabled = inputSources.includes("params"); const areHeadersEnabled = inputSources.includes("headers"); - const securityHeaders = R.chain( - R.filter((entry: Security) => entry.type === "header"), - security ?? [], - ).map(({ name }) => name); const getLocation = (name: string) => { if (areParamsEnabled && pathParams.includes(name)) return "path"; diff --git a/express-zod-api/src/documentation.ts b/express-zod-api/src/documentation.ts index d0fb7ec089..8a72632abc 100644 --- a/express-zod-api/src/documentation.ts +++ b/express-zod-api/src/documentation.ts @@ -13,7 +13,7 @@ import { contentTypes } from "./content-type"; import { DocumentationError } from "./errors"; import { getInputSources, makeCleanId } from "./common-helpers"; import { CommonConfig } from "./config-type"; -import { processContainers } from "./logical-container"; +import { processContainers, pickSecurityHeaders } from "./logical-container"; import { ClientMethod } from "./method"; import { depictBody, @@ -198,12 +198,12 @@ export class Documentation extends OpenApiBuilder { ); const request = depictRequest({ ...commons, schema: inputSchema }); - const security = processContainers(endpoint.security, maxCombinations); + const securityHeaders = pickSecurityHeaders(endpoint.security); const depictedParams = depictRequestParams({ ...commons, inputSources, isHeader, - security, + securityHeaders, request, description: descriptions?.requestParameter?.call(null, { method, @@ -254,7 +254,10 @@ export class Documentation extends OpenApiBuilder { : undefined; const securityRefs = depictSecurityRefs( - depictSecurity(security, inputSources), + depictSecurity( + processContainers(endpoint.security, maxCombinations), + inputSources, + ), scopes, (securitySchema) => { const name = this.#ensureUniqSecuritySchemaName(securitySchema); diff --git a/express-zod-api/src/logical-container.ts b/express-zod-api/src/logical-container.ts index 0d49da1172..7461cb5a02 100644 --- a/express-zod-api/src/logical-container.ts +++ b/express-zod-api/src/logical-container.ts @@ -1,5 +1,6 @@ import * as R from "ramda"; import { combinations, isObject } from "./common-helpers"; +import type { Security } from "./security"; type LogicalOr = { or: T[] }; type LogicalAnd = { and: T[] }; @@ -22,6 +23,22 @@ type Combination = T[]; /** @desc OR[ AND[a,b] , AND[b,c] ] */ export type Alternatives = Array>; +const pickHeaders = (container: LogicalContainer): string[] => { + if (isSimple(container)) { + return "type" in container && container.type === "header" + ? [container.name] + : []; + } + if (isLogicalAnd(container)) return R.chain(pickHeaders, container.and); + if (isLogicalOr(container)) return R.chain(pickHeaders, container.or); + return []; +}; + +/** @desc Extract header security names from logical containers without generating combinations */ +export const pickSecurityHeaders = ( + containers: LogicalContainer[], +): Set => new Set(R.chain(pickHeaders, containers)); + export const processContainers = ( containers: LogicalContainer[], maxCombinations = Infinity, diff --git a/express-zod-api/tests/documentation-helpers.spec.ts b/express-zod-api/tests/documentation-helpers.spec.ts index 9f5d45d373..c03845e488 100644 --- a/express-zod-api/tests/documentation-helpers.spec.ts +++ b/express-zod-api/tests/documentation-helpers.spec.ts @@ -368,7 +368,7 @@ describe("Documentation helpers", () => { { name: "authorization", expected: true }, { name: "secure", - familiar: ["secure"], + familiar: new Set(["secure"]), expected: true, }, { name: "unknown", expected: false }, @@ -464,7 +464,7 @@ describe("Documentation helpers", () => { }, inputSources: ["query", "headers", "params"], composition: "inline", - security: [[{ type: "header", name: "secure" }]], + securityHeaders: new Set(["secure"]), ...requestCtx, }), ).toMatchSnapshot(); From 204ac1abada6ff5325d551ceef44b4534b6a9979 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Sat, 25 Apr 2026 11:48:38 +0200 Subject: [PATCH 35/43] REV: rm early exists, only applying this option to combinations. --- express-zod-api/src/common-helpers.ts | 5 +-- express-zod-api/src/documentation.ts | 7 +-- express-zod-api/src/json-schema-helpers.ts | 12 ++--- express-zod-api/src/logical-container.ts | 1 - express-zod-api/src/result-helpers.ts | 6 +-- express-zod-api/tests/common-helpers.spec.ts | 7 ++- .../tests/json-schema-helpers.spec.ts | 45 ++++++++----------- .../tests/logical-container.spec.ts | 25 +++++------ express-zod-api/tests/result-helpers.spec.ts | 13 +----- 9 files changed, 47 insertions(+), 74 deletions(-) diff --git a/express-zod-api/src/common-helpers.ts b/express-zod-api/src/common-helpers.ts index 18c659a59f..29adc59f96 100644 --- a/express-zod-api/src/common-helpers.ts +++ b/express-zod-api/src/common-helpers.ts @@ -115,11 +115,10 @@ export const combinations = ( right: T[], /** @desc The function that combines elements */ merge: (a: T, b: T) => T, - /** @desc Maximum number of combinations */ + /** @desc Maximum number of combinations (only applies to Cartesian product of non-empty arrays) */ limit = Infinity, ): T[] => { - if (!(limit > 0)) return []; - if (!left.length || !right.length) return left.concat(right).slice(0, limit); + if (!left.length || !right.length) return left.concat(right); const result: T[] = []; for (let idxL = 0; idxL < left.length && result.length < limit; idxL++) { for (let idxR = 0; idxR < right.length && result.length < limit; idxR++) diff --git a/express-zod-api/src/documentation.ts b/express-zod-api/src/documentation.ts index 8a72632abc..900abd41b2 100644 --- a/express-zod-api/src/documentation.ts +++ b/express-zod-api/src/documentation.ts @@ -87,10 +87,11 @@ interface DocumentationParams { * */ tags?: Parameters[0]; /** - * @desc Maximum number of combined examples and security schemes - * @example 0 — disables combining of examples and security schemes + * @desc Limits cartesian product when combining variations. + * @desc Applies to distributed examples and security scheme alternatives. + * @example 0 — disables combinations, but keeps concatenations * @default Infinity - * @todo set to 20 in v28 + * @todo set to 20 or 50 in v28 * */ maxCombinations?: number; } diff --git a/express-zod-api/src/json-schema-helpers.ts b/express-zod-api/src/json-schema-helpers.ts index bde62c468a..b80c02f82d 100644 --- a/express-zod-api/src/json-schema-helpers.ts +++ b/express-zod-api/src/json-schema-helpers.ts @@ -89,13 +89,9 @@ export const mergeExamples = ( maxCombinations = Infinity, }: { isOptional: boolean; maxCombinations?: number }, ) => { - if (!(maxCombinations > 0)) return; if (!entry.examples?.length) return; if (isOptional) { - target.examples = R.concat(target.examples || [], entry.examples).slice( - 0, - maxCombinations, - ); + target.examples = R.concat(target.examples || [], entry.examples); } else { target.examples = combinations( target.examples?.filter(isObject) || [], @@ -148,13 +144,11 @@ export const flattenIO = ( export const pullRequestExamples = ( subject: z.core.JSONSchema.ObjectSchema, limit = Infinity, -) => { - if (!(limit > 0)) return []; - return Object.entries(subject.properties || {}).reduce( +) => + Object.entries(subject.properties || {}).reduce( (acc, [key, prop]) => { const { examples = [] } = isObject(prop) ? prop : {}; return combinations(acc, examples.map(R.objOf(key)), R.mergeRight, limit); }, [], ); -}; diff --git a/express-zod-api/src/logical-container.ts b/express-zod-api/src/logical-container.ts index 7461cb5a02..f979c79d78 100644 --- a/express-zod-api/src/logical-container.ts +++ b/express-zod-api/src/logical-container.ts @@ -43,7 +43,6 @@ export const processContainers = ( containers: LogicalContainer[], maxCombinations = Infinity, ): Alternatives => { - if (!(maxCombinations > 0)) return []; const simples = R.filter(isSimple, containers); const ands = R.chain(R.prop("and"), R.filter(isLogicalAnd, containers)); const [simpleAnds, orsInAnds] = R.partition(isSimple, ands); diff --git a/express-zod-api/src/result-helpers.ts b/express-zod-api/src/result-helpers.ts index 25ca6270a3..0fb20f96a5 100644 --- a/express-zod-api/src/result-helpers.ts +++ b/express-zod-api/src/result-helpers.ts @@ -91,13 +91,11 @@ export const getPublicErrorMessage = (error: HttpError): string => export const pullResponseExamples = ( subject: T, limit = Infinity, -) => { - if (!(limit > 0)) return []; - return Object.entries(subject._zod.def.shape).reduce( +) => + Object.entries(subject._zod.def.shape).reduce( (acc, [key, schema]) => { const { examples = [] } = globalRegistry.get(schema) || {}; return combinations(acc, examples.map(R.objOf(key)), R.mergeRight, limit); }, [], ); -}; diff --git a/express-zod-api/tests/common-helpers.spec.ts b/express-zod-api/tests/common-helpers.spec.ts index 89ddb78550..16ec43404e 100644 --- a/express-zod-api/tests/common-helpers.spec.ts +++ b/express-zod-api/tests/common-helpers.spec.ts @@ -281,11 +281,16 @@ describe("Common Helpers", () => { expect(combinations([], [], (a, b) => a + b)).toEqual([]); }); + test("should not apply the limit when returns original array", () => { + expect(combinations([1, 2, 3], [], (a, b) => a + b, 1)).toEqual([ + 1, 2, 3, + ]); + }); + test("should limit the number of combinations", () => { expect(combinations([1, 2], [4, 5, 6], (a, b) => a + b, 4)).toEqual([ 5, 6, 7, 6, ]); - expect(combinations([1, 2, 3], [], (a, b) => a + b, 1)).toEqual([1]); }); test.each([0, -1, NaN])("should return empty for limit=%s", (limit) => { diff --git a/express-zod-api/tests/json-schema-helpers.spec.ts b/express-zod-api/tests/json-schema-helpers.spec.ts index 10c2d53436..80f639c24d 100644 --- a/express-zod-api/tests/json-schema-helpers.spec.ts +++ b/express-zod-api/tests/json-schema-helpers.spec.ts @@ -203,19 +203,6 @@ describe("JSON Schema helpers", () => { expect(flat).toEqual({ type: "object", properties: {} }); }); - test.each([0, -1, NaN])( - "should do nothing when maxCombinations=%s", - (maxCombinations) => { - const flat = { type: "object" as const, properties: {} }; - mergeExamples( - flat, - { examples: [{ a: 1 }] }, - { isOptional: false, maxCombinations }, - ); - expect(flat).toEqual({ type: "object", properties: {} }); - }, - ); - test("should concatenate examples when optional", () => { const flat = { type: "object" as const, @@ -255,6 +242,24 @@ describe("JSON Schema helpers", () => { { a: 1, b: 3 }, ]); }); + + test("should apply limit to combinations when required", () => { + const flat = { + type: "object" as const, + properties: {}, + examples: [{ a: 1 }], + }; + mergeExamples( + flat, + { examples: [{ b: 2 }, { b: 3 }] }, + { isOptional: false, maxCombinations: 1 }, + ); + expect(flat).toEqual({ + type: "object", + properties: {}, + examples: [{ a: 1, b: 2 }], + }); + }); }); describe("pullRequestExamples()", () => { @@ -316,20 +321,6 @@ describe("JSON Schema helpers", () => { { name: "john", age: 30 }, ]); }); - - test.each([0, -1, NaN])("should return empty for limit=%s", (limit) => { - expect( - pullRequestExamples( - { - type: "object", - properties: { - name: { type: "string", examples: ["john"] }, - }, - }, - limit, - ), - ).toEqual([]); - }); }); describe("flattenIO()", () => { diff --git a/express-zod-api/tests/logical-container.spec.ts b/express-zod-api/tests/logical-container.spec.ts index 357a67d436..855d0b9121 100644 --- a/express-zod-api/tests/logical-container.spec.ts +++ b/express-zod-api/tests/logical-container.spec.ts @@ -124,19 +124,16 @@ describe("LogicalContainer", () => { ).toEqual([[{ type: "bearer", format: "JWT" }]]); }); - test.each([2, 0, -1, NaN])( - "should control the maximum combinations %s", - (maxCombinations) => { - expect( - processContainers( - [ - { or: [{ and: [1, 2] }, { and: [3, 4] }] }, - { or: [{ and: [5, 6] }, { and: [7, 8] }] }, - ], - maxCombinations, - ), - ).toHaveLength(Math.max(0, maxCombinations || 0)); - }, - ); + test("should control the maximum combinations %s", () => { + expect( + processContainers( + [ + { or: [{ and: [1, 2] }, { and: [3, 4] }] }, + { or: [{ and: [5, 6] }, { and: [7, 8] }] }, + ], + 2, + ), + ).toHaveLength(2); + }); }); }); diff --git a/express-zod-api/tests/result-helpers.spec.ts b/express-zod-api/tests/result-helpers.spec.ts index 4d1074d2a9..b78028f52f 100644 --- a/express-zod-api/tests/result-helpers.spec.ts +++ b/express-zod-api/tests/result-helpers.spec.ts @@ -117,18 +117,7 @@ describe("Result helpers", () => { b: z.number().meta({ examples: [1, 2] }), c: z.boolean().meta({ examples: [false] }), }); - expect(pullResponseExamples(schema, 3)).toEqual([ - { a: "one", b: 1, c: false }, - { a: "one", b: 2, c: false }, - { a: "two", b: 1, c: false }, - ]); - }); - - test.each([0, -1, NaN])("returns empty for limit=%s", (limit) => { - const schema = z.object({ - a: z.string().example("one"), - }); - expect(pullResponseExamples(schema, limit)).toEqual([]); + expect(pullResponseExamples(schema, 3)).toHaveLength(3); }); }); From 22f65637a878fc32dd69f274db8bc4f0cb4af9dd Mon Sep 17 00:00:00 2001 From: Anna Bocharova Date: Sat, 25 Apr 2026 11:52:51 +0200 Subject: [PATCH 36/43] fix(test): rm redundant placeholder Co-authored-by: pullfrog[bot] <226033991+pullfrog[bot]@users.noreply.github.com> --- express-zod-api/tests/logical-container.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/express-zod-api/tests/logical-container.spec.ts b/express-zod-api/tests/logical-container.spec.ts index 855d0b9121..4195733d19 100644 --- a/express-zod-api/tests/logical-container.spec.ts +++ b/express-zod-api/tests/logical-container.spec.ts @@ -124,7 +124,7 @@ describe("LogicalContainer", () => { ).toEqual([[{ type: "bearer", format: "JWT" }]]); }); - test("should control the maximum combinations %s", () => { + test("should control the maximum combinations", () => { expect( processContainers( [ From 18e622d3cc97da4845ebc84f5c59bb5cbbbecfa6 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Sat, 25 Apr 2026 11:56:19 +0200 Subject: [PATCH 37/43] REF: pickHeaders rearrangement, simple case goes last, fixes coverage. --- express-zod-api/src/logical-container.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/express-zod-api/src/logical-container.ts b/express-zod-api/src/logical-container.ts index f979c79d78..deb9d59c69 100644 --- a/express-zod-api/src/logical-container.ts +++ b/express-zod-api/src/logical-container.ts @@ -24,13 +24,9 @@ type Combination = T[]; export type Alternatives = Array>; const pickHeaders = (container: LogicalContainer): string[] => { - if (isSimple(container)) { - return "type" in container && container.type === "header" - ? [container.name] - : []; - } if (isLogicalAnd(container)) return R.chain(pickHeaders, container.and); if (isLogicalOr(container)) return R.chain(pickHeaders, container.or); + if (container.type === "header") return [container.name]; return []; }; From c969f228d7e6e3ba312a44290e6a7d8fef4e8c02 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Sat, 25 Apr 2026 13:05:52 +0200 Subject: [PATCH 38/43] Changelog: proposed for 27.3.0. --- CHANGELOG.md | 26 ++++++++++++++++++++++++++ express-zod-api/src/documentation.ts | 8 ++++---- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89770f34dd..cd909296b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,32 @@ ## Version 27 +### v27.3.0 + +- Introduced `maxCombinations` setting for Documentation generator: + - Limits cartesian product when generating examples by combining each property's own examples; + - Set to `0` to disable product but keep concatenations; + - Default is `Infinity`, but may change to reasonable number in v28 to avoid too many combinations; + - Example: 6 props with 2 examples each → cartesian product makes 2^6 = 64 request examples: + +```ts +const schema = z.object({ + id: z.number().example(1).example(2), + name: z.string().example("john").example("jane"), + age: z.number().example(18).example(21), + role: z.enum(["user", "admin"]).example("user").example("admin"), + active: z.boolean().example(true).example(false), + tags: z.array(z.string()).example(["vip"]).example(["new", "promo"]), +}); +// First 5: +// { id: 1, name: "john", age: 18, role: "user", active: true, tags: ["vip"] }, +// { id: 1, name: "john", age: 18, role: "user", active: true, tags: ["new", "promo"] }, +// { id: 1, name: "john", age: 18, role: "user", active: false, tags: ["vip"] }, +// { id: 1, name: "john", age: 18, role: "user", active: false, tags: ["new", "promo"] }, +// { id: 1, name: "john", age: 18, role: "admin", active: true, tags: ["vip"] }, +// ...and 59 more +``` + ### v27.2.4 - Fixed performance regression since v24.0.0: diff --git a/express-zod-api/src/documentation.ts b/express-zod-api/src/documentation.ts index 900abd41b2..b6c66a1c55 100644 --- a/express-zod-api/src/documentation.ts +++ b/express-zod-api/src/documentation.ts @@ -87,11 +87,11 @@ interface DocumentationParams { * */ tags?: Parameters[0]; /** - * @desc Limits cartesian product when combining variations. - * @desc Applies to distributed examples and security scheme alternatives. - * @example 0 — disables combinations, but keeps concatenations + * @desc Limits cartesian product when generating examples by combining each property's own examples. + * @desc Applies to: request/response examples, security scheme alternatives. + * @example 0 — disables product combinations, keeps concatenations * @default Infinity - * @todo set to 20 or 50 in v28 + * @todo set to 20 or 50 in v28 to avoid too many combinations * */ maxCombinations?: number; } From bac11b95a0d8947d9a7163bbdd4996273ea7cbff Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Sat, 25 Apr 2026 19:00:03 +0200 Subject: [PATCH 39/43] mv getSecurityHeaders into security.ts. --- express-zod-api/src/documentation.ts | 5 +++-- express-zod-api/src/logical-container.ts | 13 ------------- express-zod-api/src/security.ts | 19 +++++++++++++++++++ 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/express-zod-api/src/documentation.ts b/express-zod-api/src/documentation.ts index b6c66a1c55..6d1127f08e 100644 --- a/express-zod-api/src/documentation.ts +++ b/express-zod-api/src/documentation.ts @@ -13,7 +13,8 @@ import { contentTypes } from "./content-type"; import { DocumentationError } from "./errors"; import { getInputSources, makeCleanId } from "./common-helpers"; import { CommonConfig } from "./config-type"; -import { processContainers, pickSecurityHeaders } from "./logical-container"; +import { getSecurityHeaders } from "./security"; +import { processContainers } from "./logical-container"; import { ClientMethod } from "./method"; import { depictBody, @@ -199,7 +200,7 @@ export class Documentation extends OpenApiBuilder { ); const request = depictRequest({ ...commons, schema: inputSchema }); - const securityHeaders = pickSecurityHeaders(endpoint.security); + const securityHeaders = getSecurityHeaders(endpoint.security); const depictedParams = depictRequestParams({ ...commons, inputSources, diff --git a/express-zod-api/src/logical-container.ts b/express-zod-api/src/logical-container.ts index d9c3044de3..c71630714e 100644 --- a/express-zod-api/src/logical-container.ts +++ b/express-zod-api/src/logical-container.ts @@ -1,6 +1,5 @@ import * as R from "ramda"; import { combinations, isObject } from "./common-helpers"; -import type { Security } from "./security"; type LogicalOr = { or: T[] }; type LogicalAnd = { and: T[] }; @@ -26,18 +25,6 @@ type Combination = T[]; /** @desc OR[ AND[a,b] , AND[b,c] ] */ export type Alternatives = Array>; -const pickHeaders = (container: LogicalContainer): string[] => { - if (isLogicalAnd(container)) return R.chain(pickHeaders, container.and); - if (isLogicalOr(container)) return R.chain(pickHeaders, container.or); - if (container.type === "header") return [container.name]; - return []; -}; - -/** @desc Extract header security names from logical containers without generating combinations */ -export const pickSecurityHeaders = ( - containers: LogicalContainer[], -): Set => new Set(R.chain(pickHeaders, containers)); - export const processContainers = ( containers: LogicalContainer[], maxCombinations = Infinity, diff --git a/express-zod-api/src/security.ts b/express-zod-api/src/security.ts index 6bb64e549c..c60c9e275f 100644 --- a/express-zod-api/src/security.ts +++ b/express-zod-api/src/security.ts @@ -1,3 +1,10 @@ +import * as R from "ramda"; +import { + isLogicalAnd, + isLogicalOr, + type LogicalContainer, +} from "./logical-container"; + export interface BasicSecurity { type: "basic"; } @@ -93,3 +100,15 @@ export type Security = | CookieSecurity | OpenIdSecurity | OAuth2Security; + +const pickHeaders = (container: LogicalContainer): string[] => { + if (isLogicalAnd(container)) return R.chain(pickHeaders, container.and); + if (isLogicalOr(container)) return R.chain(pickHeaders, container.or); + if (container.type === "header") return [container.name]; + return []; +}; + +/** @desc Extract header security names from logical containers without generating combinations */ +export const getSecurityHeaders = ( + containers: LogicalContainer[], +): Set => new Set(R.chain(pickHeaders, containers)); From eb4f21cade0cb34f07df85e8ad0dc33bdbab9015 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Sat, 25 Apr 2026 20:02:30 +0200 Subject: [PATCH 40/43] tests for getSecurityHeaders. --- express-zod-api/tests/security.spec.ts | 52 ++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 express-zod-api/tests/security.spec.ts diff --git a/express-zod-api/tests/security.spec.ts b/express-zod-api/tests/security.spec.ts new file mode 100644 index 0000000000..6a8244a6ff --- /dev/null +++ b/express-zod-api/tests/security.spec.ts @@ -0,0 +1,52 @@ +import { getSecurityHeaders } from "../src/security"; + +describe("getSecurityHeaders()", () => { + test("should extract header names and ignore others", () => { + expect( + Array.from( + getSecurityHeaders([ + { type: "header", name: "Auth" }, + { type: "cookie", name: "session" }, + { type: "bearer" }, + { type: "header", name: "Key" }, + { type: "openid", url: "https://auth.example.com" }, + ]), + ), + ).toEqual(["Auth", "Key"]); + }); + + test("should handle empty array", () => { + expect(getSecurityHeaders([])).toHaveProperty("size", 0); + }); + + test.each([ + { + and: [ + { type: "header" as const, name: "A" }, + { type: "bearer" as const }, + ], + }, + { + or: [{ type: "header" as const, name: "A" }, { type: "bearer" as const }], + }, + ])("should extract headers from AND/OR %#", (container) => { + expect(Array.from(getSecurityHeaders([container]))).toEqual(["A"]); + }); + + test.each([ + { + and: [ + { type: "header" as const, name: "A1" }, + { or: [{ type: "header" as const, name: "A2" }] }, + ], + }, + { + or: [ + { type: "header" as const, name: "A1" }, + { and: [{ type: "header" as const, name: "A2" }] }, + ], + }, + ])("should extract headers from nested AND/OR", (container) => { + expect(Array.from(getSecurityHeaders([container]))).toEqual(["A1", "A2"]); + }); +}); From 540716dcdcd59318ff36440358b6a31ae3ac6bd1 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Wed, 29 Apr 2026 13:57:25 +0200 Subject: [PATCH 41/43] fix: rm default value from higher order functions calling combinations(). --- express-zod-api/src/documentation.ts | 4 ++-- express-zod-api/src/json-schema-helpers.ts | 4 ++-- express-zod-api/src/logical-container.ts | 2 +- express-zod-api/src/result-helpers.ts | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/express-zod-api/src/documentation.ts b/express-zod-api/src/documentation.ts index 6d1127f08e..a2c07d16c5 100644 --- a/express-zod-api/src/documentation.ts +++ b/express-zod-api/src/documentation.ts @@ -92,7 +92,7 @@ interface DocumentationParams { * @desc Applies to: request/response examples, security scheme alternatives. * @example 0 — disables product combinations, keeps concatenations * @default Infinity - * @todo set to 20 or 50 in v28 to avoid too many combinations + * @todo set to 10 or 20 in v28 to avoid too many combinations * */ maxCombinations?: number; } @@ -167,10 +167,10 @@ export class Documentation extends OpenApiBuilder { brandHandling, tags, isHeader, + maxCombinations, hasSummaryFromDescription = true, hasHeadMethod = true, composition = "inline", - maxCombinations = Infinity, }: DocumentationParams) { super(); this.addInfo({ title, version }); diff --git a/express-zod-api/src/json-schema-helpers.ts b/express-zod-api/src/json-schema-helpers.ts index b80c02f82d..3ceaea9903 100644 --- a/express-zod-api/src/json-schema-helpers.ts +++ b/express-zod-api/src/json-schema-helpers.ts @@ -86,7 +86,7 @@ export const mergeExamples = ( entry: z.core.JSONSchema.BaseSchema, { isOptional, - maxCombinations = Infinity, + maxCombinations, }: { isOptional: boolean; maxCombinations?: number }, ) => { if (!entry.examples?.length) return; @@ -143,7 +143,7 @@ export const flattenIO = ( /** @see pullResponseExamples */ export const pullRequestExamples = ( subject: z.core.JSONSchema.ObjectSchema, - limit = Infinity, + limit?: number, ) => Object.entries(subject.properties || {}).reduce( (acc, [key, prop]) => { diff --git a/express-zod-api/src/logical-container.ts b/express-zod-api/src/logical-container.ts index c71630714e..782b5de667 100644 --- a/express-zod-api/src/logical-container.ts +++ b/express-zod-api/src/logical-container.ts @@ -27,7 +27,7 @@ export type Alternatives = Array>; export const processContainers = ( containers: LogicalContainer[], - maxCombinations = Infinity, + maxCombinations?: number, ): Alternatives => { const simples = R.filter(isSimple, containers); const ands = R.chain(R.prop("and"), R.filter(isLogicalAnd, containers)); diff --git a/express-zod-api/src/result-helpers.ts b/express-zod-api/src/result-helpers.ts index 0fb20f96a5..04406b2fe2 100644 --- a/express-zod-api/src/result-helpers.ts +++ b/express-zod-api/src/result-helpers.ts @@ -90,7 +90,7 @@ export const getPublicErrorMessage = (error: HttpError): string => /** @see pullRequestExamples */ export const pullResponseExamples = ( subject: T, - limit = Infinity, + limit?: number, ) => Object.entries(subject._zod.def.shape).reduce( (acc, [key, schema]) => { From 415e0b8c4077fe36cb852331519d6c258cb93688 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Wed, 29 Apr 2026 21:52:45 +0200 Subject: [PATCH 42/43] fix(test): simpler assertion for the limit test of pullRequestExamples. --- express-zod-api/tests/json-schema-helpers.spec.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/express-zod-api/tests/json-schema-helpers.spec.ts b/express-zod-api/tests/json-schema-helpers.spec.ts index 80f639c24d..a00b0e61e8 100644 --- a/express-zod-api/tests/json-schema-helpers.spec.ts +++ b/express-zod-api/tests/json-schema-helpers.spec.ts @@ -316,10 +316,7 @@ describe("JSON Schema helpers", () => { }, 2, ), - ).toEqual([ - { name: "john", age: 25 }, - { name: "john", age: 30 }, - ]); + ).toHaveLength(2); }); }); From ba2f6ddbe0618c50e3eb230f53c5131a509dc11d Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Wed, 29 Apr 2026 21:55:04 +0200 Subject: [PATCH 43/43] fix(test): simpler length assertion for the limit test of mergeExamples. --- express-zod-api/tests/json-schema-helpers.spec.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/express-zod-api/tests/json-schema-helpers.spec.ts b/express-zod-api/tests/json-schema-helpers.spec.ts index a00b0e61e8..4cbbf922fa 100644 --- a/express-zod-api/tests/json-schema-helpers.spec.ts +++ b/express-zod-api/tests/json-schema-helpers.spec.ts @@ -254,11 +254,7 @@ describe("JSON Schema helpers", () => { { examples: [{ b: 2 }, { b: 3 }] }, { isOptional: false, maxCombinations: 1 }, ); - expect(flat).toEqual({ - type: "object", - properties: {}, - examples: [{ a: 1, b: 2 }], - }); + expect(flat.examples).toHaveLength(1); }); });