Skip to content

Commit 8cf0ac1

Browse files
authored
Merge pull request #901 from Edison-A-N/fix/enum-anyof-ref-resolution
fix: enhance resolution and enum handling in anyOf schemas
2 parents 17d08ff + a4aa220 commit 8cf0ac1

1 file changed

Lines changed: 49 additions & 45 deletions

File tree

client/src/utils/schemaUtils.ts

Lines changed: 49 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -159,20 +159,46 @@ export function isPropertyRequired(
159159
* Resolves $ref references in JSON schema
160160
* @param schema The schema that may contain $ref
161161
* @param rootSchema The root schema to resolve references against
162+
* @param visitedRefs Optional set of visited $ref paths to detect circular references
162163
* @returns The resolved schema without $ref
163164
*/
164165
export function resolveRef(
165166
schema: JsonSchemaType,
166167
rootSchema: JsonSchemaType,
168+
visitedRefs: Set<string> = new Set(),
167169
): JsonSchemaType {
170+
if (!schema) return schema;
171+
168172
if (!("$ref" in schema) || !schema.$ref) {
173+
// Recursively resolve $ref in anyOf (and other nested structures)
174+
if (schema.anyOf && Array.isArray(schema.anyOf)) {
175+
const resolvedAnyOf = schema.anyOf.map((item) => {
176+
if (typeof item === "object" && item !== null) {
177+
return resolveRef(item, rootSchema, visitedRefs);
178+
}
179+
return item;
180+
});
181+
return {
182+
...schema,
183+
anyOf: resolvedAnyOf,
184+
};
185+
}
169186
return schema;
170187
}
171188

172189
const ref = schema.$ref;
173190

174-
// Handle simple #/properties/name references
191+
// Handle all #/ formats (#/properties/, #/$defs/, etc.)
175192
if (ref.startsWith("#/")) {
193+
// Check for circular reference
194+
if (visitedRefs.has(ref)) {
195+
console.warn(`Circular reference detected: ${ref}`);
196+
return schema;
197+
}
198+
199+
// Add current ref to visited set
200+
visitedRefs.add(ref);
201+
176202
const path = ref.substring(2).split("/");
177203
let current: unknown = rootSchema;
178204

@@ -186,12 +212,16 @@ export function resolveRef(
186212
current = (current as Record<string, unknown>)[segment];
187213
} else {
188214
// If reference cannot be resolved, return the original schema
215+
visitedRefs.delete(ref); // Clean up on failure
189216
console.warn(`Could not resolve $ref: ${ref}`);
190217
return schema;
191218
}
192219
}
193220

194-
return current as JsonSchemaType;
221+
const resolved = current as JsonSchemaType;
222+
223+
// Recursively resolve nested structures (anyOf, oneOf, items, properties)
224+
return resolveRef(resolved, rootSchema, visitedRefs);
195225
}
196226

197227
// For other types of references, return the original schema
@@ -205,54 +235,28 @@ export function resolveRef(
205235
* @returns A normalized schema or the original schema
206236
*/
207237
export function normalizeUnionType(schema: JsonSchemaType): JsonSchemaType {
208-
// Handle anyOf with exactly string and null (FastMCP pattern)
238+
// Handle anyOf with exactly 2 items (type and null) - unified handling
239+
// Preserves enum and other properties automatically
209240
if (
210241
schema.anyOf &&
211242
schema.anyOf.length === 2 &&
212-
schema.anyOf.some((t) => (t as JsonSchemaType).type === "string") &&
213243
schema.anyOf.some((t) => (t as JsonSchemaType).type === "null")
214244
) {
215-
return { ...schema, type: "string", anyOf: undefined, nullable: true };
216-
}
217-
218-
// Handle anyOf with exactly boolean and null (FastMCP pattern)
219-
if (
220-
schema.anyOf &&
221-
schema.anyOf.length === 2 &&
222-
schema.anyOf.some((t) => (t as JsonSchemaType).type === "boolean") &&
223-
schema.anyOf.some((t) => (t as JsonSchemaType).type === "null")
224-
) {
225-
return { ...schema, type: "boolean", anyOf: undefined, nullable: true };
226-
}
227-
228-
// Handle anyOf with exactly number and null (FastMCP pattern)
229-
if (
230-
schema.anyOf &&
231-
schema.anyOf.length === 2 &&
232-
schema.anyOf.some((t) => (t as JsonSchemaType).type === "number") &&
233-
schema.anyOf.some((t) => (t as JsonSchemaType).type === "null")
234-
) {
235-
return { ...schema, type: "number", anyOf: undefined, nullable: true };
236-
}
237-
238-
// Handle anyOf with exactly integer and null (FastMCP pattern)
239-
if (
240-
schema.anyOf &&
241-
schema.anyOf.length === 2 &&
242-
schema.anyOf.some((t) => (t as JsonSchemaType).type === "integer") &&
243-
schema.anyOf.some((t) => (t as JsonSchemaType).type === "null")
244-
) {
245-
return { ...schema, type: "integer", anyOf: undefined, nullable: true };
246-
}
247-
248-
// Handle anyOf with exactly array and null (FastMCP pattern)
249-
if (
250-
schema.anyOf &&
251-
schema.anyOf.length === 2 &&
252-
schema.anyOf.some((t) => (t as JsonSchemaType).type === "array") &&
253-
schema.anyOf.some((t) => (t as JsonSchemaType).type === "null")
254-
) {
255-
return { ...schema, type: "array", anyOf: undefined, nullable: true };
245+
const nonNullItem = schema.anyOf.find((t) => {
246+
const item = t as JsonSchemaType;
247+
return item?.type !== "null";
248+
}) as JsonSchemaType;
249+
250+
// Only process if non-null item has type or enum
251+
if (nonNullItem?.type || nonNullItem?.enum) {
252+
return {
253+
...schema,
254+
...nonNullItem,
255+
type: nonNullItem?.type || (nonNullItem?.enum ? "string" : undefined),
256+
nullable: true,
257+
anyOf: undefined,
258+
};
259+
}
256260
}
257261

258262
// Handle array type with exactly string and null

0 commit comments

Comments
 (0)