From 81d38f1e51245412ffa4ffc77f339374b811484b Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Mon, 20 Jul 2026 19:14:58 -0400 Subject: [PATCH 1/2] fix(json-schema): stop forType custom attributes leaking across interned primitive types (#1268) Co-Authored-By: gpt-5.6-sol via pi --- .../src/input/JSONSchemaInput.ts | 41 +++++++- .../json-schema-custom-attributes.test.ts | 97 +++++++++++++++++++ 2 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 test/unit/json-schema-custom-attributes.test.ts diff --git a/packages/quicktype-core/src/input/JSONSchemaInput.ts b/packages/quicktype-core/src/input/JSONSchemaInput.ts index fe9efcbd8..4114dad8f 100644 --- a/packages/quicktype-core/src/input/JSONSchemaInput.ts +++ b/packages/quicktype-core/src/input/JSONSchemaInput.ts @@ -33,6 +33,7 @@ import { descriptionAttributeProducer } from "../attributes/Description.js"; import { enumValuesAttributeProducer } from "../attributes/EnumValues.js"; import { StringTypes } from "../attributes/StringTypes.js"; import { + TypeAttributeKind, type TypeAttributes, combineTypeAttributes, emptyTypeAttributes, @@ -665,6 +666,40 @@ export type JSONSchemaAttributeProducer = ( unionCases: JSONSchema[] | undefined, ) => JSONSchemaAttributes | undefined; +// `forType` attributes belong to one schema occurrence. Keep primitive types +// carrying them out of the intern pool so the attributes cannot leak to other +// occurrences of the same primitive kind. +class ForTypeIdentityTypeAttributeKind extends TypeAttributeKind { + public constructor() { + super("forTypeIdentity"); + } + + public combine(_attributes: true[]): true { + return true; + } + + public makeInferred(_attribute: true): true { + return true; + } + + public requiresUniqueIdentity(_attribute: true): boolean { + return true; + } +} + +const forTypeIdentityTypeAttributeKind = new ForTypeIdentityTypeAttributeKind(); + +function makeForTypeAttributesUnique( + attributes: TypeAttributes, +): TypeAttributes { + if (attributes.size === 0) return attributes; + return combineTypeAttributes( + "union", + attributes, + forTypeIdentityTypeAttributeKind.makeAttributes(true), + ); +} + function typeKindForJSONSchemaFormat( format: string, ): TransformedStringTypeKind | undefined { @@ -989,7 +1024,9 @@ async function addTypesInSchema( forCases === undefined, "We can't have attributes for unions and cases if we don't have a union", ); - return forType; + return forType === undefined + ? undefined + : makeForTypeAttributesUnique(forType); }, ), ); @@ -1217,7 +1254,7 @@ async function addTypesInSchema( if (forType !== undefined) { typeBuilder.addAttributes( intersectionType, - forType, + makeForTypeAttributesUnique(forType), ); } diff --git a/test/unit/json-schema-custom-attributes.test.ts b/test/unit/json-schema-custom-attributes.test.ts new file mode 100644 index 000000000..1f6e4a027 --- /dev/null +++ b/test/unit/json-schema-custom-attributes.test.ts @@ -0,0 +1,97 @@ +import { + FetchingJSONSchemaStore, + InputData, + type JSONSchema, + JSONSchemaInput, + type Type, + TypeAttributeKind, + quicktype, +} from "quicktype-core"; +import { expect, test } from "vitest"; + +class DeprecatedTypeAttributeKind extends TypeAttributeKind { + public constructor() { + super("deprecated"); + } + + public combine(attributes: boolean[]): boolean { + return attributes.some((deprecated) => deprecated); + } + + public makeInferred(): boolean { + return false; + } + + public addToSchema( + schema: { [name: string]: unknown }, + _type: Type, + deprecated: boolean, + ): void { + if (deprecated) schema.deprecated = true; + } +} + +const deprecatedTypeAttributeKind = new DeprecatedTypeAttributeKind(); + +function deprecatedAttributeProducer(schema: JSONSchema) { + if ( + typeof schema !== "object" || + schema === null || + schema.deprecated !== true + ) { + return undefined; + } + + return { + forType: deprecatedTypeAttributeKind.makeAttributes(true), + }; +} + +test("a forType custom attribute does not leak between primitive schema nodes (issue #1268)", async () => { + const schemaInput = new JSONSchemaInput(new FetchingJSONSchemaStore(), [ + deprecatedAttributeProducer, + ]); + await schemaInput.addSource({ + name: "Player", + schema: JSON.stringify({ + type: "object", + properties: { + externalId: { type: "string" }, + participant: { $ref: "#/definitions/Participant" }, + }, + required: ["externalId", "participant"], + definitions: { + Participant: { + type: "object", + properties: { + emailAddress: { type: "string", deprecated: true }, + phoneNumber: { type: "string", deprecated: true }, + }, + required: ["emailAddress", "phoneNumber"], + }, + }, + }), + }); + const inputData = new InputData(); + inputData.addInput(schemaInput); + + const result = await quicktype({ inputData, lang: "schema" }); + const output = JSON.parse(result.lines.join("\n")) as { + definitions: Record< + string, + { properties: Record> } + >; + }; + const playerProperties = output.definitions.Player.properties; + const participantProperties = output.definitions.Participant.properties; + + expect(playerProperties.externalId).not.toHaveProperty("deprecated"); + expect(participantProperties.emailAddress).toHaveProperty( + "deprecated", + true, + ); + expect(participantProperties.phoneNumber).toHaveProperty( + "deprecated", + true, + ); +}); From 6472066432e7562fea462296b52a18d118bbf68b Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Wed, 22 Jul 2026 15:38:58 -0400 Subject: [PATCH 2/2] fix(json-schema): isolate only custom forType attributes --- .../src/input/JSONSchemaInput.ts | 27 +++++++++++++------ .../json-schema-custom-attributes.test.ts | 12 +++------ 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/packages/quicktype-core/src/input/JSONSchemaInput.ts b/packages/quicktype-core/src/input/JSONSchemaInput.ts index 56fcaf713..7d3436fae 100644 --- a/packages/quicktype-core/src/input/JSONSchemaInput.ts +++ b/packages/quicktype-core/src/input/JSONSchemaInput.ts @@ -669,9 +669,9 @@ export type JSONSchemaAttributeProducer = ( unionCases: JSONSchema[] | undefined, ) => JSONSchemaAttributes | undefined; -// `forType` attributes belong to one schema occurrence. Keep primitive types -// carrying them out of the intern pool so the attributes cannot leak to other -// occurrences of the same primitive kind. +// `forType` attributes from custom producers belong to one schema occurrence. +// Keep primitive types carrying them out of the intern pool so the attributes +// cannot leak to other occurrences of the same primitive kind. class ForTypeIdentityTypeAttributeKind extends TypeAttributeKind { public constructor() { super("forTypeIdentity"); @@ -703,6 +703,19 @@ function makeForTypeAttributesUnique( ); } +function makeCustomAttributeProducer( + producer: JSONSchemaAttributeProducer, +): JSONSchemaAttributeProducer { + return (schema, canonicalRef, types, unionCases) => { + const attributes = producer(schema, canonicalRef, types, unionCases); + if (attributes?.forType === undefined) return attributes; + return { + ...attributes, + forType: makeForTypeAttributesUnique(attributes.forType), + }; + }; +} + function typeKindForJSONSchemaFormat( format: string, ): TransformedStringTypeKind | undefined { @@ -1027,9 +1040,7 @@ async function addTypesInSchema( forCases === undefined, "We can't have attributes for unions and cases if we don't have a union", ); - return forType === undefined - ? undefined - : makeForTypeAttributesUnique(forType); + return forType; }, ), ); @@ -1260,7 +1271,7 @@ async function addTypesInSchema( if (forType !== undefined) { typeBuilder.addAttributes( intersectionType, - makeForTypeAttributesUnique(forType), + forType, ); } @@ -1613,7 +1624,7 @@ export class JSONSchemaInput implements Input { minMaxLengthAttributeProducer, minMaxItemsAttributeProducer, patternAttributeProducer, - ].concat(additionalAttributeProducers); + ].concat(additionalAttributeProducers.map(makeCustomAttributeProducer)); } public get needIR(): boolean { diff --git a/test/unit/json-schema-custom-attributes.test.ts b/test/unit/json-schema-custom-attributes.test.ts index 1f6e4a027..9006e7918 100644 --- a/test/unit/json-schema-custom-attributes.test.ts +++ b/test/unit/json-schema-custom-attributes.test.ts @@ -34,16 +34,12 @@ class DeprecatedTypeAttributeKind extends TypeAttributeKind { const deprecatedTypeAttributeKind = new DeprecatedTypeAttributeKind(); function deprecatedAttributeProducer(schema: JSONSchema) { - if ( - typeof schema !== "object" || - schema === null || - schema.deprecated !== true - ) { - return undefined; - } + if (typeof schema !== "object" || schema === null) return undefined; return { - forType: deprecatedTypeAttributeKind.makeAttributes(true), + forType: deprecatedTypeAttributeKind.makeAttributes( + schema.deprecated === true, + ), }; }