Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 49 additions & 1 deletion packages/quicktype-core/src/input/JSONSchemaInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,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,
Expand Down Expand Up @@ -668,6 +669,53 @@ export type JSONSchemaAttributeProducer = (
unionCases: JSONSchema[] | undefined,
) => JSONSchemaAttributes | undefined;

// `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<true> {
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 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 {
Expand Down Expand Up @@ -1576,7 +1624,7 @@ export class JSONSchemaInput implements Input<JSONSchemaSourceData> {
minMaxLengthAttributeProducer,
minMaxItemsAttributeProducer,
patternAttributeProducer,
].concat(additionalAttributeProducers);
].concat(additionalAttributeProducers.map(makeCustomAttributeProducer));
}

public get needIR(): boolean {
Expand Down
93 changes: 93 additions & 0 deletions test/unit/json-schema-custom-attributes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import {
FetchingJSONSchemaStore,
InputData,
type JSONSchema,
JSONSchemaInput,
type Type,
TypeAttributeKind,
quicktype,
} from "quicktype-core";
import { expect, test } from "vitest";

class DeprecatedTypeAttributeKind extends TypeAttributeKind<boolean> {
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) return undefined;

return {
forType: deprecatedTypeAttributeKind.makeAttributes(
schema.deprecated === 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<string, Record<string, unknown>> }
>;
};
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,
);
});
Loading