fix: support JSON Schema spec compliant oneOf with const for labeled enums

- Make JsonSchemaType.type optional to support const-only schemas
- Add JsonSchemaConst type for schema objects using const values
- Update oneOf/anyOf to accept both full schemas and const schemas
- Add comprehensive tests for oneOf with const and title patterns

This fixes TypeScript compilation errors when using the JSON Schema
specification's recommended approach for labeled enums:
{ "type": "string", "oneOf": [{ "const": "red", "title": "Stop" }] }

Replaces deprecated enumNames pattern with spec-compliant implementation.
This commit is contained in:
Alex Andru 2025-07-04 19:39:03 +02:00
commit 3a121bc8fc
2 changed files with 116 additions and 10 deletions

View file

@ -345,8 +345,11 @@ describe("JsonSchemaType elicitation field support", () => {
},
role: {
type: "string",
enum: ["admin", "user", "guest"],
enumNames: ["Administrator", "User", "Guest"],
oneOf: [
{ const: "admin", title: "Administrator" },
{ const: "user", title: "User" },
{ const: "guest", title: "Guest" },
],
},
},
required: ["name", "email"],
@ -372,15 +375,17 @@ describe("JsonSchemaType elicitation field support", () => {
);
});
test("should handle enum and enumNames fields", () => {
test("should handle oneOf with const and title fields", () => {
const schema = {
type: "string" as const,
enum: ["option1", "option2"],
enumNames: ["Option 1", "Option 2"],
type: "string",
oneOf: [
{ const: "option1", title: "Option 1" },
{ const: "option2", title: "Option 2" },
],
};
expect(getValueAtPath(schema, ["enum", "0"])).toBe("option1");
expect(getValueAtPath(schema, ["enumNames", "1"])).toBe("Option 2");
expect(getValueAtPath(schema, ["oneOf", "0", "const"])).toBe("option1");
expect(getValueAtPath(schema, ["oneOf", "1", "title"])).toBe("Option 2");
});
test("should handle validation constraints", () => {
@ -425,4 +430,97 @@ describe("JsonSchemaType elicitation field support", () => {
"Do you accept the terms and conditions?",
);
});
test("should handle JSON Schema spec compliant oneOf with const for labeled enums", () => {
// Example from JSON Schema spec: labeled enums using oneOf with const
const trafficLightSchema = {
type: "string" as const,
title: "Traffic Light",
description: "Select a traffic light color",
oneOf: [
{ const: "red", title: "Stop" },
{ const: "amber", title: "Caution" },
{ const: "green", title: "Go" },
],
};
// Verify the schema structure
expect(trafficLightSchema.type).toBe("string");
expect(trafficLightSchema.oneOf).toHaveLength(3);
// Verify each oneOf option has const and title
expect(trafficLightSchema.oneOf[0].const).toBe("red");
expect(trafficLightSchema.oneOf[0].title).toBe("Stop");
expect(trafficLightSchema.oneOf[1].const).toBe("amber");
expect(trafficLightSchema.oneOf[1].title).toBe("Caution");
expect(trafficLightSchema.oneOf[2].const).toBe("green");
expect(trafficLightSchema.oneOf[2].title).toBe("Go");
// Test with JsonValue operations
const schemaAsJsonValue = trafficLightSchema as JsonValue;
expect(getValueAtPath(schemaAsJsonValue, ["oneOf", "0", "const"])).toBe(
"red",
);
expect(getValueAtPath(schemaAsJsonValue, ["oneOf", "1", "title"])).toBe(
"Caution",
);
expect(getValueAtPath(schemaAsJsonValue, ["oneOf", "2", "const"])).toBe(
"green",
);
});
test("should handle complex oneOf scenarios with mixed schema types", () => {
const complexSchema = {
type: "object" as const,
title: "User Preference",
properties: {
theme: {
type: "string" as const,
oneOf: [
{ const: "light", title: "Light Mode" },
{ const: "dark", title: "Dark Mode" },
{ const: "auto", title: "Auto (System)" },
],
},
notifications: {
type: "string" as const,
oneOf: [
{ const: "all", title: "All Notifications" },
{ const: "important", title: "Important Only" },
{ const: "none", title: "None" },
],
},
},
};
expect(
getValueAtPath(complexSchema, [
"properties",
"theme",
"oneOf",
"0",
"const",
]),
).toBe("light");
expect(
getValueAtPath(complexSchema, [
"properties",
"theme",
"oneOf",
"1",
"title",
]),
).toBe("Dark Mode");
expect(
getValueAtPath(complexSchema, [
"properties",
"notifications",
"oneOf",
"2",
"const",
]),
).toBe("none");
});
});

View file

@ -7,8 +7,14 @@ export type JsonValue =
| JsonValue[]
| { [key: string]: JsonValue };
export type JsonSchemaConst = {
const: JsonValue;
title?: string;
description?: string;
};
export type JsonSchemaType = {
type:
type?:
| "string"
| "number"
| "integer"
@ -29,7 +35,9 @@ export type JsonSchemaType = {
pattern?: string;
format?: string;
enum?: string[];
enumNames?: string[];
const?: JsonValue;
oneOf?: (JsonSchemaType | JsonSchemaConst)[];
anyOf?: (JsonSchemaType | JsonSchemaConst)[];
};
export type JsonObject = { [key: string]: JsonValue };