From 0c4d4c3b7bdc9c0261a40060fd4475520a0fd26b Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Tue, 21 Apr 2026 07:43:25 +0200 Subject: [PATCH 1/8] ref(flattenIO): Reducing complexity below 20. --- eslint.config.js | 1 + express-zod-api/src/json-schema-helpers.ts | 72 ++++--- .../tests/json-schema-helpers.spec.ts | 183 +++++++++++++++++- 3 files changed, 230 insertions(+), 26 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 66aa6a6fcf..60798d6f9c 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -216,6 +216,7 @@ export default tsPlugin.config( name: "source/ez", files: ["express-zod-api/src/*.ts"], rules: { + complexity: ["error", 20], "allowed/dependencies": ["error", { packageDir: ezDir }], "no-restricted-syntax": [ "warn", diff --git a/express-zod-api/src/json-schema-helpers.ts b/express-zod-api/src/json-schema-helpers.ts index 1d96da0daa..9dab5b8d22 100644 --- a/express-zod-api/src/json-schema-helpers.ts +++ b/express-zod-api/src/json-schema-helpers.ts @@ -2,6 +2,8 @@ import * as R from "ramda"; import { combinations, FlatObject, isObject } from "./common-helpers"; import type { z } from "zod"; +export type MergeMode = "coerce" | "throw"; + const isJsonObjectSchema = ( subject: z.core.JSONSchema.BaseSchema, ): subject is z.core.JSONSchema.ObjectSchema => subject.type === "object"; @@ -27,9 +29,50 @@ const canMerge = R.pipe( const nestOptional = R.pair(true); +export const processAllOf = ( + entry: z.core.JSONSchema.BaseSchema, + mode: MergeMode, + isOptional: boolean, +) => { + if (!("allOf" in entry) || !entry.allOf) return []; + return entry.allOf.map((one) => { + if (mode === "throw" && !(one.type === "object" && canMerge(one))) + throw new Error("Can not merge"); + return R.pair(isOptional, one); + }); +}; + +export const processVariants = (entry: z.core.JSONSchema.BaseSchema) => { + const result: [boolean, z.core.JSONSchema.BaseSchema][] = []; + if (entry.anyOf) result.push(...R.map(nestOptional, entry.anyOf)); + if (entry.oneOf) result.push(...R.map(nestOptional, entry.oneOf)); + return result; +}; + +export const processPropertyNames = ( + entry: z.core.JSONSchema.BaseSchema, + flat: z.core.JSONSchema.ObjectSchema & + Required>, + flatRequired: string[], + isOptional: boolean, +) => { + if (!isObject(entry.propertyNames)) return; + const keys: string[] = []; + if (typeof entry.propertyNames.const === "string") + keys.push(entry.propertyNames.const); + if (entry.propertyNames.enum) { + keys.push( + ...entry.propertyNames.enum.filter((one) => typeof one === "string"), + ); + } + const value = { ...Object(entry.additionalProperties) }; // it can be bool + for (const key of keys) flat.properties[key] ??= value; + if (!isOptional) flatRequired.push(...keys); +}; + export const flattenIO = ( jsonSchema: z.core.JSONSchema.BaseSchema, - mode: "coerce" | "throw" = "coerce", + mode: MergeMode = "coerce", ) => { const stack = [R.pair(false, jsonSchema)]; // [isOptional, JSON Schema] const flat: z.core.JSONSchema.ObjectSchema & @@ -41,17 +84,8 @@ export const flattenIO = ( while (stack.length) { const [isOptional, entry] = stack.shift()!; if (entry.description) flat.description ??= entry.description; - if (entry.allOf) { - stack.push( - ...entry.allOf.map((one) => { - if (mode === "throw" && !(one.type === "object" && canMerge(one))) - throw new Error("Can not merge"); - return R.pair(isOptional, one); - }), - ); - } - if (entry.anyOf) stack.push(...R.map(nestOptional, entry.anyOf)); - if (entry.oneOf) stack.push(...R.map(nestOptional, entry.oneOf)); + stack.push(...processAllOf(entry, mode, isOptional)); + stack.push(...processVariants(entry)); if (entry.examples?.length) { if (isOptional) { flat.examples = R.concat(flat.examples || [], entry.examples); @@ -72,19 +106,7 @@ export const flattenIO = ( ); if (!isOptional && entry.required) flatRequired.push(...entry.required); } - if (isObject(entry.propertyNames)) { - const keys: string[] = []; - if (typeof entry.propertyNames.const === "string") - keys.push(entry.propertyNames.const); - if (entry.propertyNames.enum) { - keys.push( - ...entry.propertyNames.enum.filter((one) => typeof one === "string"), - ); - } - const value = { ...Object(entry.additionalProperties) }; // it can be bool - for (const key of keys) flat.properties[key] ??= value; - if (!isOptional) flatRequired.push(...keys); - } + processPropertyNames(entry, flat, flatRequired, isOptional); } if (flatRequired.length) flat.required = [...new Set(flatRequired)]; return flat; diff --git a/express-zod-api/tests/json-schema-helpers.spec.ts b/express-zod-api/tests/json-schema-helpers.spec.ts index df3458fe02..7df60b9d5f 100644 --- a/express-zod-api/tests/json-schema-helpers.spec.ts +++ b/express-zod-api/tests/json-schema-helpers.spec.ts @@ -1,7 +1,188 @@ import { z } from "zod"; -import { flattenIO } from "../src/json-schema-helpers"; +import { + flattenIO, + processAllOf, + processVariants, + processPropertyNames, + pullRequestExamples, +} from "../src/json-schema-helpers"; describe("JSON Schema helpers", () => { + describe("processAllOf()", () => { + test("should return empty array when no allOf", () => { + const result = processAllOf( + { type: "object", properties: {} }, + "coerce", + false, + ); + expect(result).toEqual([]); + }); + + test("should map allOf entries with isOptional flag in coerce mode", () => { + const result = processAllOf( + { + type: "object", + allOf: [{ type: "object", properties: { a: { type: "string" } } }], + }, + "coerce", + true, + ); + expect(result).toEqual([ + [true, { type: "object", properties: { a: { type: "string" } } }], + ]); + }); + + test("should throw in throw mode when schema cannot be merged", () => { + expect(() => + processAllOf( + { type: "object", allOf: [{ type: "string" }] }, + "throw", + false, + ), + ).toThrow("Can not merge"); + }); + + test("should allow mergeable schemas in throw mode", () => { + const result = processAllOf( + { + type: "object", + allOf: [{ type: "object", properties: { a: { type: "string" } } }], + }, + "throw", + false, + ); + expect(result).toEqual([ + [false, { type: "object", properties: { a: { type: "string" } } }], + ]); + }); + }); + + describe("processVariants()", () => { + test("should return empty array when no anyOf/oneOf", () => { + const result = processVariants({ type: "object", properties: {} }); + expect(result).toEqual([]); + }); + + test("should process anyOf as optional", () => { + const result = processVariants({ + type: "object", + anyOf: [{ type: "string" }, { type: "number" }], + }); + expect(result).toEqual([ + [true, { type: "string" }], + [true, { type: "number" }], + ]); + }); + + test("should process oneOf as optional", () => { + const result = processVariants({ + type: "object", + oneOf: [{ type: "string" }, { type: "number" }], + }); + expect(result).toEqual([ + [true, { type: "string" }], + [true, { type: "number" }], + ]); + }); + }); + + describe("processPropertyNames()", () => { + test("should not modify flat when no propertyNames", () => { + const flat = { type: "object" as const, properties: {} }; + const flatRequired: string[] = []; + processPropertyNames( + { type: "object", properties: {} }, + flat, + flatRequired, + false, + ); + expect(flat.properties).toEqual({}); + expect(flatRequired).toEqual([]); + }); + + test("should extract const key", () => { + const flat = { type: "object" as const, properties: {} }; + const flatRequired: string[] = []; + processPropertyNames( + { type: "object", propertyNames: { const: "key" } }, + flat, + flatRequired, + false, + ); + expect(flat.properties).toHaveProperty("key"); + expect(flatRequired).toContain("key"); + }); + + test("should extract enum keys", () => { + const flat = { type: "object" as const, properties: {} }; + const flatRequired: string[] = []; + processPropertyNames( + { type: "object", propertyNames: { enum: ["a", "b"] } }, + flat, + flatRequired, + false, + ); + expect(flat.properties).toHaveProperty("a"); + expect(flat.properties).toHaveProperty("b"); + expect(flatRequired).toContain("a"); + expect(flatRequired).toContain("b"); + }); + + test("should not add to required when optional", () => { + const entry = { + type: "object" as const, + propertyNames: { const: "key" }, + }; + const flat = { type: "object" as const, properties: {} }; + const flatRequired: string[] = []; + processPropertyNames(entry, flat, flatRequired, true); + expect(flatRequired).toEqual([]); + }); + }); + + describe("pullRequestExamples()", () => { + test("should return empty array for empty properties", () => { + expect(pullRequestExamples({ type: "object", properties: {} })).toEqual( + [], + ); + }); + + test("should return empty array when properties have no examples", () => { + expect( + pullRequestExamples({ + type: "object", + properties: { name: { type: "string" } }, + }), + ).toEqual([]); + }); + + test("should extract examples from properties", () => { + expect( + pullRequestExamples({ + type: "object", + properties: { name: { type: "string", examples: ["john", "jane"] } }, + }), + ).toEqual([{ name: "john" }, { name: "jane" }]); + }); + + test("should combine examples from multiple properties", () => { + expect( + pullRequestExamples({ + type: "object", + properties: { + name: { type: "string", examples: ["john", "jane"] }, + age: { type: "number", examples: [25, 30] }, + }, + }), + ).toEqual([ + { name: "john", age: 25 }, + { name: "john", age: 30 }, + { name: "jane", age: 25 }, + { name: "jane", age: 30 }, + ]); + }); + }); + describe("flattenIO()", () => { test("should pass the object schema through", () => { const subject = flattenIO({ From f82cc6ec4c3bda24b0f8fc697b6d8218559ef708 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Tue, 21 Apr 2026 08:15:30 +0200 Subject: [PATCH 2/8] fix(test): add assertion. --- express-zod-api/tests/json-schema-helpers.spec.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 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 7df60b9d5f..835ae6ea73 100644 --- a/express-zod-api/tests/json-schema-helpers.spec.ts +++ b/express-zod-api/tests/json-schema-helpers.spec.ts @@ -129,13 +129,15 @@ describe("JSON Schema helpers", () => { }); test("should not add to required when optional", () => { - const entry = { - type: "object" as const, - propertyNames: { const: "key" }, - }; const flat = { type: "object" as const, properties: {} }; const flatRequired: string[] = []; - processPropertyNames(entry, flat, flatRequired, true); + processPropertyNames( + { type: "object", propertyNames: { const: "key" } }, + flat, + flatRequired, + true, + ); + expect(flat.properties).toHaveProperty("key"); expect(flatRequired).toEqual([]); }); }); From d2f5aa5cd70895390d8c4af92b20f6aaaa62eea3 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Tue, 21 Apr 2026 08:18:04 +0200 Subject: [PATCH 3/8] fix: marking internal helpers. --- express-zod-api/src/json-schema-helpers.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/express-zod-api/src/json-schema-helpers.ts b/express-zod-api/src/json-schema-helpers.ts index 9dab5b8d22..8395d2cc73 100644 --- a/express-zod-api/src/json-schema-helpers.ts +++ b/express-zod-api/src/json-schema-helpers.ts @@ -29,6 +29,7 @@ const canMerge = R.pipe( const nestOptional = R.pair(true); +/** @internal */ export const processAllOf = ( entry: z.core.JSONSchema.BaseSchema, mode: MergeMode, @@ -42,6 +43,7 @@ export const processAllOf = ( }); }; +/** @internal */ export const processVariants = (entry: z.core.JSONSchema.BaseSchema) => { const result: [boolean, z.core.JSONSchema.BaseSchema][] = []; if (entry.anyOf) result.push(...R.map(nestOptional, entry.anyOf)); @@ -49,6 +51,7 @@ export const processVariants = (entry: z.core.JSONSchema.BaseSchema) => { return result; }; +/** @internal */ export const processPropertyNames = ( entry: z.core.JSONSchema.BaseSchema, flat: z.core.JSONSchema.ObjectSchema & From 10cc31cc6686b8b2132a45c1869f592d672a2f71 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Tue, 21 Apr 2026 08:36:13 +0200 Subject: [PATCH 4/8] fix(test): more tests for existing lowest level helpers. --- express-zod-api/src/json-schema-helpers.ts | 12 +++-- .../tests/json-schema-helpers.spec.ts | 54 +++++++++++++++++++ 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/express-zod-api/src/json-schema-helpers.ts b/express-zod-api/src/json-schema-helpers.ts index 8395d2cc73..7cfd397002 100644 --- a/express-zod-api/src/json-schema-helpers.ts +++ b/express-zod-api/src/json-schema-helpers.ts @@ -4,17 +4,20 @@ import type { z } from "zod"; export type MergeMode = "coerce" | "throw"; -const isJsonObjectSchema = ( +/** @internal */ +export const isJsonObjectSchema = ( subject: z.core.JSONSchema.BaseSchema, ): subject is z.core.JSONSchema.ObjectSchema => subject.type === "object"; -const propsMerger = R.mergeDeepWith((a: unknown, b: unknown) => { +/** @internal */ +export const propsMerger = R.mergeDeepWith((a: unknown, b: unknown) => { if (Array.isArray(a) && Array.isArray(b)) return R.concat(a, b); if (a === b) return b; throw new Error("Can not flatten properties", { cause: { a, b } }); }); -const canMerge = R.pipe( +/** @internal */ +export const canMerge = R.pipe( Object.keys, R.without([ "type", @@ -27,7 +30,8 @@ const canMerge = R.pipe( R.isEmpty, ); -const nestOptional = R.pair(true); +/** @internal */ +export const nestOptional = R.pair(true); /** @internal */ export const processAllOf = ( diff --git a/express-zod-api/tests/json-schema-helpers.spec.ts b/express-zod-api/tests/json-schema-helpers.spec.ts index 835ae6ea73..66384d241f 100644 --- a/express-zod-api/tests/json-schema-helpers.spec.ts +++ b/express-zod-api/tests/json-schema-helpers.spec.ts @@ -1,6 +1,10 @@ import { z } from "zod"; import { flattenIO, + isJsonObjectSchema, + propsMerger, + canMerge, + nestOptional, processAllOf, processVariants, processPropertyNames, @@ -8,6 +12,56 @@ import { } from "../src/json-schema-helpers"; describe("JSON Schema helpers", () => { + describe("isJsonObjectSchema()", () => { + test("should return true for object schema", () => { + expect(isJsonObjectSchema({ type: "object" })).toBe(true); + }); + + test.each(["string", "array"] as const)( + "should return false for non-object schema", + (one) => { + expect(isJsonObjectSchema({ type: one })).toBe(false); + }, + ); + }); + + describe("propsMerger()", () => { + test("should merge objects deeply", () => { + expect(propsMerger({ a: { b: 1 } }, { a: { c: 2 } })).toStrictEqual({ + a: { b: 1, c: 2 }, + }); + }); + + test("should throw when leaf values cannot be merged", () => { + expect(() => propsMerger({ a: 1 }, { a: "string" })).toThrow( + "Can not flatten properties", + ); + }); + }); + + describe("canMerge()", () => { + test("should return true for empty object", () => { + expect(canMerge({})).toBe(true); + }); + + test("should return true for object with only mergeable keys", () => { + expect(canMerge({ type: "object", properties: {} })).toBe(true); + }); + + test.each([{ title: "test" }, { format: "date-time" }])( + "should return false for object with non-mergeable keys", + (subj) => { + expect(canMerge(subj)).toBe(false); + }, + ); + }); + + describe("nestOptional()", () => { + test("should pair true with given argument", () => { + expect(nestOptional("value")).toEqual([true, "value"]); + }); + }); + describe("processAllOf()", () => { test("should return empty array when no allOf", () => { const result = processAllOf( From 69bba26026752f5926d6e595df48c2567b614b6a Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Tue, 21 Apr 2026 08:47:50 +0200 Subject: [PATCH 5/8] ref(minor): arg naming. --- express-zod-api/src/json-schema-helpers.ts | 45 ++++++++++------------ 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/express-zod-api/src/json-schema-helpers.ts b/express-zod-api/src/json-schema-helpers.ts index 7cfd397002..aa2d1c83b6 100644 --- a/express-zod-api/src/json-schema-helpers.ts +++ b/express-zod-api/src/json-schema-helpers.ts @@ -2,7 +2,9 @@ import * as R from "ramda"; import { combinations, FlatObject, isObject } from "./common-helpers"; import type { z } from "zod"; -export type MergeMode = "coerce" | "throw"; +type MergeMode = "coerce" | "throw"; +type FlattenObjectSchema = z.core.JSONSchema.ObjectSchema & + Required>; /** @internal */ export const isJsonObjectSchema = ( @@ -35,12 +37,12 @@ export const nestOptional = R.pair(true); /** @internal */ export const processAllOf = ( - entry: z.core.JSONSchema.BaseSchema, + subject: z.core.JSONSchema.BaseSchema, mode: MergeMode, isOptional: boolean, ) => { - if (!("allOf" in entry) || !entry.allOf) return []; - return entry.allOf.map((one) => { + if (!("allOf" in subject) || !subject.allOf) return []; + return subject.allOf.map((one) => { if (mode === "throw" && !(one.type === "object" && canMerge(one))) throw new Error("Can not merge"); return R.pair(isOptional, one); @@ -48,33 +50,32 @@ export const processAllOf = ( }; /** @internal */ -export const processVariants = (entry: z.core.JSONSchema.BaseSchema) => { +export const processVariants = (subject: z.core.JSONSchema.BaseSchema) => { const result: [boolean, z.core.JSONSchema.BaseSchema][] = []; - if (entry.anyOf) result.push(...R.map(nestOptional, entry.anyOf)); - if (entry.oneOf) result.push(...R.map(nestOptional, entry.oneOf)); + if (subject.anyOf) result.push(...R.map(nestOptional, subject.anyOf)); + if (subject.oneOf) result.push(...R.map(nestOptional, subject.oneOf)); return result; }; /** @internal */ export const processPropertyNames = ( - entry: z.core.JSONSchema.BaseSchema, - flat: z.core.JSONSchema.ObjectSchema & - Required>, - flatRequired: string[], + subject: z.core.JSONSchema.BaseSchema, + target: FlattenObjectSchema, + requiredKeys: string[], isOptional: boolean, ) => { - if (!isObject(entry.propertyNames)) return; + if (!isObject(subject.propertyNames)) return; const keys: string[] = []; - if (typeof entry.propertyNames.const === "string") - keys.push(entry.propertyNames.const); - if (entry.propertyNames.enum) { + if (typeof subject.propertyNames.const === "string") + keys.push(subject.propertyNames.const); + if (subject.propertyNames.enum) { keys.push( - ...entry.propertyNames.enum.filter((one) => typeof one === "string"), + ...subject.propertyNames.enum.filter((one) => typeof one === "string"), ); } - const value = { ...Object(entry.additionalProperties) }; // it can be bool - for (const key of keys) flat.properties[key] ??= value; - if (!isOptional) flatRequired.push(...keys); + const value = { ...Object(subject.additionalProperties) }; // it can be bool + for (const key of keys) target.properties[key] ??= value; + if (!isOptional) requiredKeys.push(...keys); }; export const flattenIO = ( @@ -82,11 +83,7 @@ export const flattenIO = ( mode: MergeMode = "coerce", ) => { const stack = [R.pair(false, jsonSchema)]; // [isOptional, JSON Schema] - const flat: z.core.JSONSchema.ObjectSchema & - Required> = { - type: "object", - properties: {}, - }; + const flat: FlattenObjectSchema = { type: "object", properties: {} }; const flatRequired: string[] = []; while (stack.length) { const [isOptional, entry] = stack.shift()!; From 6ed1e5e73fe23bc017813fd9571eba3c592eb4a7 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Tue, 21 Apr 2026 12:03:32 +0200 Subject: [PATCH 6/8] fix(processPropertyNames): narrowing the argument type. --- 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 aa2d1c83b6..997987c15b 100644 --- a/express-zod-api/src/json-schema-helpers.ts +++ b/express-zod-api/src/json-schema-helpers.ts @@ -59,7 +59,7 @@ export const processVariants = (subject: z.core.JSONSchema.BaseSchema) => { /** @internal */ export const processPropertyNames = ( - subject: z.core.JSONSchema.BaseSchema, + subject: z.core.JSONSchema.ObjectSchema, target: FlattenObjectSchema, requiredKeys: string[], isOptional: boolean, From 0fbc239fbaa11241952c0dd2fcae279fbe055839 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Tue, 21 Apr 2026 13:59:47 +0200 Subject: [PATCH 7/8] fix(nestOptional): narrowing the type. --- express-zod-api/src/json-schema-helpers.ts | 2 +- express-zod-api/tests/json-schema-helpers.spec.ts | 5 ++++- 2 files 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 997987c15b..4b2b73d51e 100644 --- a/express-zod-api/src/json-schema-helpers.ts +++ b/express-zod-api/src/json-schema-helpers.ts @@ -33,7 +33,7 @@ export const canMerge = R.pipe( ); /** @internal */ -export const nestOptional = R.pair(true); +export const nestOptional = R.pair(true); /** @internal */ export const processAllOf = ( diff --git a/express-zod-api/tests/json-schema-helpers.spec.ts b/express-zod-api/tests/json-schema-helpers.spec.ts index 66384d241f..b18c08016b 100644 --- a/express-zod-api/tests/json-schema-helpers.spec.ts +++ b/express-zod-api/tests/json-schema-helpers.spec.ts @@ -58,7 +58,10 @@ describe("JSON Schema helpers", () => { describe("nestOptional()", () => { test("should pair true with given argument", () => { - expect(nestOptional("value")).toEqual([true, "value"]); + expect(nestOptional({ type: "string" })).toEqual([ + true, + { type: "string" }, + ]); }); }); From 27e75ec162466b4a120aa04cc85be11c0efa2ab7 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Tue, 21 Apr 2026 14:06:18 +0200 Subject: [PATCH 8/8] ref: extracting the Stack type. --- express-zod-api/src/json-schema-helpers.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/express-zod-api/src/json-schema-helpers.ts b/express-zod-api/src/json-schema-helpers.ts index 4b2b73d51e..04d94a164c 100644 --- a/express-zod-api/src/json-schema-helpers.ts +++ b/express-zod-api/src/json-schema-helpers.ts @@ -34,6 +34,7 @@ export const canMerge = R.pipe( /** @internal */ export const nestOptional = R.pair(true); +type Stack = Array>; /** @internal */ export const processAllOf = ( @@ -51,7 +52,7 @@ export const processAllOf = ( /** @internal */ export const processVariants = (subject: z.core.JSONSchema.BaseSchema) => { - const result: [boolean, z.core.JSONSchema.BaseSchema][] = []; + const result: Stack = []; if (subject.anyOf) result.push(...R.map(nestOptional, subject.anyOf)); if (subject.oneOf) result.push(...R.map(nestOptional, subject.oneOf)); return result; @@ -82,7 +83,7 @@ export const flattenIO = ( jsonSchema: z.core.JSONSchema.BaseSchema, mode: MergeMode = "coerce", ) => { - const stack = [R.pair(false, jsonSchema)]; // [isOptional, JSON Schema] + const stack: Stack = [R.pair(false, jsonSchema)]; // [isOptional, JSON Schema] const flat: FlattenObjectSchema = { type: "object", properties: {} }; const flatRequired: string[] = []; while (stack.length) {