From 3788af88afe701cc6a438a9ad60d014b63f0b792 Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Mon, 20 Jul 2026 19:32:34 -0400 Subject: [PATCH 1/3] fix(schema): union allOf properties instead of intersecting them (#1152) When allOf combined a $ref to a closed object schema (additionalProperties: false) with an inline closed object schema, the intersection accumulator dropped any property that wasn't declared by every branch, instead of unioning the declared properties across branches. additionalProperties: false should only restrict names outside that union, not cause named properties from one branch to be discarded because another branch didn't also declare them. This produced classes with missing (sometimes all) properties. Add a schema fixture (allof-closed-objects.schema) with a positive sample proving all properties from both allOf branches round-trip, and a negative sample proving the required "discriminator" property is still enforced. Fixes #1152 Co-Authored-By: gpt-5.6-sol via pi --- .../src/rewrites/ResolveIntersections.ts | 16 +++----- .../schema/allof-closed-objects.1.fail.json | 6 +++ .../inputs/schema/allof-closed-objects.1.json | 7 ++++ .../inputs/schema/allof-closed-objects.schema | 37 +++++++++++++++++++ 4 files changed, 55 insertions(+), 11 deletions(-) create mode 100644 test/inputs/schema/allof-closed-objects.1.fail.json create mode 100644 test/inputs/schema/allof-closed-objects.1.json create mode 100644 test/inputs/schema/allof-closed-objects.schema diff --git a/packages/quicktype-core/src/rewrites/ResolveIntersections.ts b/packages/quicktype-core/src/rewrites/ResolveIntersections.ts index 7f3ab80d84..2d391542ec 100644 --- a/packages/quicktype-core/src/rewrites/ResolveIntersections.ts +++ b/packages/quicktype-core/src/rewrites/ResolveIntersections.ts @@ -20,7 +20,7 @@ import { makeTypeAttributesInferred, } from "../attributes/TypeAttributes.js"; import type { GraphRewriteBuilder, TypeLookerUp } from "../GraphRewriting.js"; -import { assert, defined, mustNotHappen, panic } from "../support/Support.js"; +import { assert, defined, panic } from "../support/Support.js"; import { ArrayType, GenericClassProperty, @@ -183,6 +183,9 @@ class IntersectionAccumulator return; } + // Named properties from intersected objects are combined. Whether + // additional properties are allowed only affects names not declared + // by any of those objects. const allPropertyNames = setUnionInto( new Set(this._objectProperties.keys()), maybeObject.getProperties().keys(), @@ -206,12 +209,7 @@ class IntersectionAccumulator existing.isOptional, ); defined(this._objectProperties).set(name, cp); - } else if (existing !== undefined) { - defined(this._objectProperties).delete(name); - } else if ( - newProperty !== undefined && - this._additionalPropertyTypes !== undefined - ) { + } else if (newProperty !== undefined) { // FIXME: This is potentially slow const types = new Set(this._additionalPropertyTypes).add( newProperty.type, @@ -220,10 +218,6 @@ class IntersectionAccumulator name, new GenericClassProperty(types, newProperty.isOptional), ); - } else if (newProperty !== undefined) { - defined(this._objectProperties).delete(name); - } else { - mustNotHappen(); } } diff --git a/test/inputs/schema/allof-closed-objects.1.fail.json b/test/inputs/schema/allof-closed-objects.1.fail.json new file mode 100644 index 0000000000..a145c7895e --- /dev/null +++ b/test/inputs/schema/allof-closed-objects.1.fail.json @@ -0,0 +1,6 @@ +{ + "animal": { + "foo": "inherited property", + "bar": "inline property" + } +} diff --git a/test/inputs/schema/allof-closed-objects.1.json b/test/inputs/schema/allof-closed-objects.1.json new file mode 100644 index 0000000000..a5e433dd2d --- /dev/null +++ b/test/inputs/schema/allof-closed-objects.1.json @@ -0,0 +1,7 @@ +{ + "animal": { + "foo": "inherited property", + "discriminator": "dog", + "bar": "inline property" + } +} diff --git a/test/inputs/schema/allof-closed-objects.schema b/test/inputs/schema/allof-closed-objects.schema new file mode 100644 index 0000000000..35727a2764 --- /dev/null +++ b/test/inputs/schema/allof-closed-objects.schema @@ -0,0 +1,37 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Container", + "type": "object", + "additionalProperties": false, + "properties": { + "animal": { + "oneOf": [ + { "$ref": "#/definitions/dog" }, + { "type": "null" } + ] + } + }, + "definitions": { + "dog": { + "allOf": [ + { "$ref": "#/definitions/animal" }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "bar": { "type": ["null", "string"] } + } + } + ] + }, + "animal": { + "type": "object", + "additionalProperties": false, + "required": ["discriminator"], + "properties": { + "foo": { "type": ["null", "string"] }, + "discriminator": { "type": "string" } + } + } + } +} From abaf1ef7571f34400e93afe228e9a8f35fd9726d Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Mon, 20 Jul 2026 20:55:00 -0400 Subject: [PATCH 2/3] fix CI: gate allof-closed-objects required-property fail sample by no-defaults feature (#1152) The negative fixture sample allof-closed-objects.1.fail.json was named without a feature suffix, so test/fixtures.ts treated it as "every language must reject this input" regardless of that language's `features` list. Not all languages enforce JSON Schema `required` at deserialization time; Go in particular silently defaults a missing string field to "". Renaming it to allof-closed-objects.1.fail.no-defaults.json matches the existing convention used by required.1.fail.no-defaults.json, so the negative case only runs for languages generated with the no-defaults feature. Co-Authored-By: gpt-5.6-sol via pi --- ...s.1.fail.json => allof-closed-objects.1.fail.no-defaults.json} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/inputs/schema/{allof-closed-objects.1.fail.json => allof-closed-objects.1.fail.no-defaults.json} (100%) diff --git a/test/inputs/schema/allof-closed-objects.1.fail.json b/test/inputs/schema/allof-closed-objects.1.fail.no-defaults.json similarity index 100% rename from test/inputs/schema/allof-closed-objects.1.fail.json rename to test/inputs/schema/allof-closed-objects.1.fail.no-defaults.json From 41a19f2b199eab482dfd58bf7a6fc4644cc1b009 Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Tue, 21 Jul 2026 11:30:44 -0400 Subject: [PATCH 3/3] fix CI: skip allof-closed-objects.schema for languages that don't enforce required properties The allof-closed-objects.1.fail.no-defaults.json negative sample expects the generated program to reject an object missing the required "discriminator" property. Gating it by the no-defaults feature is not sufficient: several languages list no-defaults for other reasons yet do not enforce required properties at deserialization time, so the sample does not fail for them and the fixture reports an unexpected success. These are exactly the languages that already skip required.schema / intersection.schema for the same reason: - cjson: required properties absent are not checked - swift: on Linux the missing-required failure case doesn't fail - haskell: a failed decode prints "null" and exits 0, so expected-failure samples cannot be detected - elixir: struct keys cannot be enforced at runtime and are set to null Add allof-closed-objects.schema to their skipSchema lists, mirroring the existing required.schema handling. The positive sample and the negative sample still run for every language that does enforce required (csharp, cplusplus, python, rust, kotlin, scala3, typescript, typescript-zod, typescript-effect-schema, ...), keeping at least one positive and one negative case running. Co-Authored-By: Claude --- test/languages.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/languages.ts b/test/languages.ts index ecbe724eef..a75f2c4cac 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -646,6 +646,7 @@ export const CJSONLanguage: Language = { /* Required properties absent are not checked (for the current implementation, can be added later, should abord parsing and return NULL) */ "intersection.schema", "required.schema", + "allof-closed-objects.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", "direct-union.schema", @@ -967,6 +968,8 @@ export const SwiftLanguage: Language = { // This works on macOS, but on Linux one of the failure test cases doesn't fail ...skipsUntypedUnions, "required.schema", + // Same Linux-only missing-required behavior as required.schema. + "allof-closed-objects.schema", "multi-type-enum.schema", "intersection.schema", ...skipsMapValueValidation, @@ -1895,6 +1898,9 @@ export const HaskellLanguage: Language = { "keyword-unions.schema", "optional-any.schema", "required.schema", + // Same undetectable expected-failure limitation as required.schema: + // a failed decode prints "null" and exits 0. + "allof-closed-objects.schema", "required-non-properties.schema", ], rendererOptions: {}, @@ -2239,6 +2245,7 @@ export const ElixirLanguage: Language = { // Struct keys cannot be enforced at runtime in Elixir and their values will just be set to null. "strict-optional.schema", "required.schema", + "allof-closed-objects.schema", "boolean-subschema.schema", "intersection.schema", "optional-any.schema",