From 849a5330c35a7fb5c2160840d9c6db50d998e7a1 Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Mon, 20 Jul 2026 17:44:36 -0400 Subject: [PATCH 1/4] fix(schema): preserve property declaration order from JSON Schema (#2698) JSON Schema input always sorted object properties by lowercased name at type-building time in makeObject (JSONSchemaInput.ts), discarding the schema's declaration order before the alphabetizeProperties renderer option ever got a chance to apply. JSON (non-schema) input was unaffected since it doesn't go through this schema-specific sort. Make sortKey optional in makeObject and only sort when a key is given (quicktypePropertyOrder still supplies one when the schema declares it). Otherwise properties keep their original insertion order, matching JSON input, and alphabetizeProperties:true continues to sort at render time. Adds a schema fixture (property-order.schema/.1.json) covering both default declaration order and quicktypePropertyOrder, verified via a generated-vs-source key-order check in the typescript fixture driver. Co-Authored-By: gpt-5.6-sol via pi --- .../src/input/JSONSchemaInput.ts | 29 ++++++++++--------- test/fixtures/typescript/main.ts | 12 ++++++++ test/inputs/schema/property-order.1.json | 12 ++++++++ test/inputs/schema/property-order.schema | 24 +++++++++++++++ 4 files changed, 63 insertions(+), 14 deletions(-) create mode 100644 test/inputs/schema/property-order.1.json create mode 100644 test/inputs/schema/property-order.schema diff --git a/packages/quicktype-core/src/input/JSONSchemaInput.ts b/packages/quicktype-core/src/input/JSONSchemaInput.ts index efd2ccbef9..53610369e5 100644 --- a/packages/quicktype-core/src/input/JSONSchemaInput.ts +++ b/packages/quicktype-core/src/input/JSONSchemaInput.ts @@ -822,13 +822,14 @@ async function addTypesInSchema( properties: StringMap, requiredArray: string[], additionalProperties: unknown, - sortKey: (k: string) => number | string = (k: string): string => - k.toLowerCase(), + sortKey?: (k: string) => number | string, ): Promise { const required = new Set(requiredArray); - const propertiesMap = mapSortBy(mapFromObject(properties), (_, k) => - sortKey(k), - ); + const unsortedPropertiesMap = mapFromObject(properties); + const propertiesMap = + sortKey === undefined + ? unsortedPropertiesMap + : mapSortBy(unsortedPropertiesMap, (_, k) => sortKey(k)); const props = await mapMapSync( propertiesMap, async (propSchema, propName) => { @@ -1156,15 +1157,15 @@ async function addTypesInSchema( inferredAttributes, combineProducedAttributes(({ forObject }) => forObject), ); - const order = schema.quicktypePropertyOrder - ? schema.quicktypePropertyOrder - : []; - const orderKey = (propertyName: string): string => { - // use the index of the order array - const index = order.indexOf(propertyName); - // if no index then use the property name - return index !== -1 ? index : propertyName.toLowerCase(); - }; + const order = schema.quicktypePropertyOrder; + const orderKey = order + ? (propertyName: string): string => { + // use the index of the order array + const index = order.indexOf(propertyName); + // if no index then use the property name + return index !== -1 ? index : propertyName.toLowerCase(); + } + : undefined; return await makeObject( loc, diff --git a/test/fixtures/typescript/main.ts b/test/fixtures/typescript/main.ts index c22518dd29..b955f52827 100644 --- a/test/fixtures/typescript/main.ts +++ b/test/fixtures/typescript/main.ts @@ -10,4 +10,16 @@ const json = fs.readFileSync(sample); const value = TopLevel.Convert.toTopLevel(json); const backToJson = TopLevel.Convert.topLevelToJson(value); +if (sample.endsWith("property-order.1.json")) { + const input = JSON.parse(json.toString()); + const output = JSON.parse(backToJson); + const keys = (object: object) => JSON.stringify(Object.keys(object)); + if ( + keys(input) !== keys(output) || + keys(input.ordered) !== keys(output.ordered) + ) { + throw new Error("Generated property order does not match the schema"); + } +} + console.log(backToJson); diff --git a/test/inputs/schema/property-order.1.json b/test/inputs/schema/property-order.1.json new file mode 100644 index 0000000000..5e65476d1c --- /dev/null +++ b/test/inputs/schema/property-order.1.json @@ -0,0 +1,12 @@ +{ + "zebra": "stripes", + "mango": 1.5, + "apple": true, + "delta": "change", + "banana": 3, + "ordered": { + "mango": 2.5, + "zebra": "stripes", + "apple": false + } +} diff --git a/test/inputs/schema/property-order.schema b/test/inputs/schema/property-order.schema new file mode 100644 index 0000000000..64a7652236 --- /dev/null +++ b/test/inputs/schema/property-order.schema @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "zebra": { "type": "string" }, + "mango": { "type": "number" }, + "apple": { "type": "boolean" }, + "delta": { "type": "string" }, + "banana": { "type": "integer" }, + "ordered": { + "type": "object", + "quicktypePropertyOrder": ["mango", "zebra"], + "properties": { + "zebra": { "type": "string" }, + "mango": { "type": "number" }, + "apple": { "type": "boolean" } + }, + "required": ["zebra", "mango", "apple"], + "additionalProperties": false + } + }, + "required": ["zebra", "mango", "apple", "delta", "banana", "ordered"], + "additionalProperties": false +} From 4327126d4b0c68e10332ddadc9f1beabc74c7ce4 Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Mon, 20 Jul 2026 19:38:43 -0400 Subject: [PATCH 2/4] test: add missing fixture cases for property-order.schema (#3016) Co-Authored-By: Claude --- .../schema/property-order.1.fail.no-defaults.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 test/inputs/schema/property-order.1.fail.no-defaults.json diff --git a/test/inputs/schema/property-order.1.fail.no-defaults.json b/test/inputs/schema/property-order.1.fail.no-defaults.json new file mode 100644 index 0000000000..7addfdb867 --- /dev/null +++ b/test/inputs/schema/property-order.1.fail.no-defaults.json @@ -0,0 +1,11 @@ +{ + "zebra": "stripes", + "mango": 1.5, + "apple": true, + "delta": "change", + "ordered": { + "mango": 2.5, + "zebra": "stripes", + "apple": false + } +} From a4c581db0ca112706c3b2ba84dbd14962366ab12 Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Tue, 21 Jul 2026 08:19:06 -0400 Subject: [PATCH 3/4] fix CI: skip property-order.schema for cjson, which cannot detect missing required properties (#undefined) cjson silently defaults a missing required field to 0 instead of failing, the same known limitation already documented for required.schema and intersection.schema. This matched CI's property-order.1.fail.no-defaults.json exiting 0 instead of failing as expected. Co-Authored-By: gpt-5.6-sol via pi --- test/languages.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/languages.ts b/test/languages.ts index 7f80f5e3b1..0c97e29de7 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -612,6 +612,7 @@ export const CJSONLanguage: Language = { "pattern.schema", /* Required properties absent are not checked (for the current implementation, can be added later, should abord parsing and return NULL) */ "intersection.schema", + "property-order.schema", "required.schema", /* Pure Any type not supported (for the current implementation, can be added later, should manage a callback to provide the final application a way to handle it at parsing and creation of cJSON) */ "any.schema", From b3a37d84b8f5dc5999f6b146319157afcd2e8d8b Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Tue, 21 Jul 2026 08:36:22 -0400 Subject: [PATCH 4/4] fix CI: skip property-order.schema for elixir and haskell, which cannot detect missing required properties (#undefined) Same root cause as the earlier cjson fix on this branch: both languages already skip required.schema/intersection.schema for the identical documented reason (Elixir sets absent struct keys to null at runtime; Haskell's fixture driver serializes a failed Maybe decode as JSON null and exits 0), so property-order.1.fail.no-defaults.json (omits required "banana", no default) cannot be detected as a failure by either. Co-Authored-By: gpt-5.6-sol via pi --- test/languages.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/languages.ts b/test/languages.ts index 55ad7fbce8..5180416bfb 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -1867,6 +1867,7 @@ export const HaskellLanguage: Language = { "multi-type-enum.schema", "keyword-unions.schema", "optional-any.schema", + "property-order.schema", "required.schema", "required-non-properties.schema", ], @@ -2212,6 +2213,7 @@ export const ElixirLanguage: Language = { "strict-optional.schema", "required.schema", "intersection.schema", + "property-order.schema", // The test incorrectly succeeds due to the emitter being permissive for unions that contain only primitives. A future enhancement // for the Elixir emitter could be a user-controlled 'strict' mode that pattern matches even on unions of only primitive types.