From af0c9ff51cb5e60bc12e372838a214e38479d6bc Mon Sep 17 00:00:00 2001 From: haya14busa Date: Sat, 5 Jul 2025 22:24:21 +0900 Subject: [PATCH 1/3] feat: Add support for unevaluatedProperties in JSON Schema Add support for the unevaluatedProperties keyword from JSON Schema draft 2019-09/2020-12. When additionalProperties is not defined, unevaluatedProperties is now used as a fallback to determine the type of additional properties in an object. This fixes issues where objects using unevaluatedProperties would generate incorrect types (e.g., map[string]interface{} instead of properly typed maps in Go). --- .../src/input/JSONSchemaInput.ts | 5 ++ .../schema/unevaluated-properties.1.json | 14 ++++++ .../schema/unevaluated-properties.2.json | 19 ++++++++ .../schema/unevaluated-properties.schema | 46 +++++++++++++++++++ 4 files changed, 84 insertions(+) create mode 100644 test/inputs/schema/unevaluated-properties.1.json create mode 100644 test/inputs/schema/unevaluated-properties.2.json create mode 100644 test/inputs/schema/unevaluated-properties.schema diff --git a/packages/quicktype-core/src/input/JSONSchemaInput.ts b/packages/quicktype-core/src/input/JSONSchemaInput.ts index 6907c0e488..0ce3152918 100644 --- a/packages/quicktype-core/src/input/JSONSchemaInput.ts +++ b/packages/quicktype-core/src/input/JSONSchemaInput.ts @@ -1108,6 +1108,11 @@ async function addTypesInSchema( ) { additionalProperties = schema.patternProperties[".*"]; } + + // Handle unevaluatedProperties if additionalProperties is not defined + if (additionalProperties === undefined && schema.unevaluatedProperties !== undefined) { + additionalProperties = schema.unevaluatedProperties; + } const objectAttributes = combineTypeAttributes( "union", diff --git a/test/inputs/schema/unevaluated-properties.1.json b/test/inputs/schema/unevaluated-properties.1.json new file mode 100644 index 0000000000..a8e22fbc6a --- /dev/null +++ b/test/inputs/schema/unevaluated-properties.1.json @@ -0,0 +1,14 @@ +{ + "config": { + "name": "test-config", + "settings": { + "option1": [ + {"key": "foo", "value": "bar"}, + {"key": "baz", "value": "qux"} + ], + "option2": [ + {"key": "hello", "value": "world"} + ] + } + } +} \ No newline at end of file diff --git a/test/inputs/schema/unevaluated-properties.2.json b/test/inputs/schema/unevaluated-properties.2.json new file mode 100644 index 0000000000..a739f3e56c --- /dev/null +++ b/test/inputs/schema/unevaluated-properties.2.json @@ -0,0 +1,19 @@ +{ + "config": { + "name": "multiple-versions", + "settings": { + "v1.0.0": [ + {"key": "checksum1", "value": "abc123"}, + {"key": "checksum2", "value": "def456"} + ], + "v1.1.0": [ + {"key": "checksum1", "value": "ghi789"} + ], + "latest": [ + {"key": "checksum1", "value": "jkl012"}, + {"key": "checksum2", "value": "mno345"}, + {"key": "checksum3", "value": "pqr678"} + ] + } + } +} \ No newline at end of file diff --git a/test/inputs/schema/unevaluated-properties.schema b/test/inputs/schema/unevaluated-properties.schema new file mode 100644 index 0000000000..1e1382a192 --- /dev/null +++ b/test/inputs/schema/unevaluated-properties.schema @@ -0,0 +1,46 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "unevaluated-properties.schema", + "title": "Test unevaluatedProperties support", + "type": "object", + "properties": { + "config": { + "$ref": "#/$defs/Config" + } + }, + "$defs": { + "Config": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "settings": { + "$ref": "#/$defs/Settings" + } + } + }, + "Settings": { + "type": "object", + "properties": {}, + "unevaluatedProperties": { + "type": "array", + "items": { + "$ref": "#/$defs/Item" + } + } + }, + "Item": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": ["key", "value"] + } + } +} \ No newline at end of file From 27c1f845041387ade8a6bf58939486fef57c1beb Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Mon, 20 Jul 2026 10:21:40 -0400 Subject: [PATCH 2/3] Fix biome formatting in JSONSchemaInput.ts Co-Authored-By: Claude Fable 5 --- packages/quicktype-core/src/input/JSONSchemaInput.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/quicktype-core/src/input/JSONSchemaInput.ts b/packages/quicktype-core/src/input/JSONSchemaInput.ts index 4e534ee19d..5a931731e9 100644 --- a/packages/quicktype-core/src/input/JSONSchemaInput.ts +++ b/packages/quicktype-core/src/input/JSONSchemaInput.ts @@ -1138,9 +1138,12 @@ async function addTypesInSchema( ) { additionalProperties = schema.patternProperties[".*"]; } - + // Handle unevaluatedProperties if additionalProperties is not defined - if (additionalProperties === undefined && schema.unevaluatedProperties !== undefined) { + if ( + additionalProperties === undefined && + schema.unevaluatedProperties !== undefined + ) { additionalProperties = schema.unevaluatedProperties; } From 61f95a0feebeaab10a2ff5bdcd8369bbb48c3c19 Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Mon, 20 Jul 2026 11:15:37 -0400 Subject: [PATCH 3/3] test(schema): prove unevaluatedProperties values are typed, not any MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A map and a map round-trip the positive samples identically, so add an expected-failure sample with a mistyped map value. Languages that don't reject mistyped map values skip it via a new shared skipsMapValueValidation list, which also absorbs the existing go-schema-pattern-properties.schema entries — the same failure mode. Co-Authored-By: Claude Fable 5 --- .../schema/unevaluated-properties.1.fail.json | 8 ++++++ test/languages.ts | 27 ++++++++++++------- 2 files changed, 26 insertions(+), 9 deletions(-) create mode 100644 test/inputs/schema/unevaluated-properties.1.fail.json diff --git a/test/inputs/schema/unevaluated-properties.1.fail.json b/test/inputs/schema/unevaluated-properties.1.fail.json new file mode 100644 index 0000000000..1b5ea63b1f --- /dev/null +++ b/test/inputs/schema/unevaluated-properties.1.fail.json @@ -0,0 +1,8 @@ +{ + "config": { + "name": "test-config", + "settings": { + "option1": "not-an-item-list" + } + } +} diff --git a/test/languages.ts b/test/languages.ts index 6bcfd43611..94921b7593 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -46,6 +46,15 @@ const skipsUntypedUnions = [ "implicit-class-array-union.schema", ]; +// The generated code does not reject wrong-typed values in typed maps (a +// map accepts values that are not T), so the bare `.fail.json` +// samples for these map-valued schemas do not fail as expected. Add any new +// schema whose fail sample relies on rejecting a mistyped map value. +const skipsMapValueValidation = [ + "go-schema-pattern-properties.schema", + "unevaluated-properties.schema", +]; + export type LanguageFeature = | "enum" | "union" @@ -576,7 +585,7 @@ export const CJSONLanguage: Language = { ...skipsEnumValueValidation, /* Union, Map and Arrays with invalid types are not checked (for the current implementation, can be added later, should abord parsing and return NULL) */ "class-with-additional.schema", - "go-schema-pattern-properties.schema", + ...skipsMapValueValidation, "multi-type-enum.schema", "nested-intersection-union.schema", "prefix-items.schema", @@ -889,7 +898,7 @@ export const SwiftLanguage: Language = { "required.schema", "multi-type-enum.schema", "intersection.schema", - "go-schema-pattern-properties.schema", + ...skipsMapValueValidation, ...skipsEnumValueValidation, "date-time.schema", "class-with-additional.schema", @@ -1317,7 +1326,7 @@ export const KotlinLanguage: Language = { // instead of rejecting it. "nested-intersection-union.schema", "class-with-additional.schema", - "go-schema-pattern-properties.schema", + ...skipsMapValueValidation, // IllegalArgumentException "accessors.schema", "description.schema", @@ -1408,7 +1417,7 @@ export const KotlinJacksonLanguage: Language = { // instead of rejecting it. "nested-intersection-union.schema", "class-with-additional.schema", - "go-schema-pattern-properties.schema", + ...skipsMapValueValidation, // IllegalArgumentException "accessors.schema", "description.schema", @@ -1675,7 +1684,7 @@ export const PikeLanguage: Language = { // no implicit cast int <-> float in Pike ...skipsIntFloatUnions, // all below: not failing on expected failure. That's because Pike's quite tolerant with assignments. - "go-schema-pattern-properties.schema", + ...skipsMapValueValidation, "class-with-additional.schema", "multi-type-enum.schema", ...skipsUntypedUnions, @@ -1764,7 +1773,7 @@ export const HaskellLanguage: Language = { "prefix-items.schema", "direct-union.schema", ...skipsEnumValueValidation, - "go-schema-pattern-properties.schema", + ...skipsMapValueValidation, "intersection.schema", "multi-type-enum.schema", "keyword-unions.schema", @@ -1933,7 +1942,7 @@ export const TypeScriptZodLanguage: Language = { "constructor.schema", // z.coerce.date() serializes back as ISO UTC, not the input string "date-time.schema", - "go-schema-pattern-properties.schema", + ...skipsMapValueValidation, "intersection.schema", "multi-type-enum.schema", "keyword-unions.schema", @@ -2049,7 +2058,7 @@ export const TypeScriptEffectSchemaLanguage: Language = { ...skipsUntypedUnions, "direct-union.schema", ...skipsEnumValueValidation, - "go-schema-pattern-properties.schema", + ...skipsMapValueValidation, "intersection.schema", "multi-type-enum.schema", "keyword-unions.schema", @@ -2116,7 +2125,7 @@ export const ElixirLanguage: Language = { // 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. - "go-schema-pattern-properties.schema", + ...skipsMapValueValidation, // The generated top-level type is not emitted as a TopLevel module the fixture can call. "recursive-union-flattening.schema",