libs/core/mcp/src/utils/json-schema-ref-resolver.ts
JSON Schema Reference Resolver
This utility resolves $ref pointers in JSON schemas by:
[key: string]:
|
const { get } = require('lodash-es');
/**
* JSON Schema Reference Resolver
*
* This utility resolves $ref pointers in JSON schemas by:
* 1. Starting at a sub-schema and identifying all $refs
* 2. Loading external schema references and placing them in a definitions block
* 3. Updating $refs to point to local JSON Pointers
* 4. Recursively processing nested $refs within resolved definitions
* 5. Preventing circular references by checking already-processed schemas
*/
interface JsonSchema {
[key: string]: unknown;
}
interface ResolverContext {
fullSchema: JsonSchema;
resolvedReferences: Record<string, JsonSchema>;
}
function resolveReference(ref: string, schema: JsonSchema): JsonSchema | 'Failed to resolve' {
// Only handle local references starting with "#/"
if (!ref.startsWith('#/')) {
return 'Failed to resolve';
}
const path = ref.substring(2).split('/').join('.');
return get(schema, path, 'Failed to resolve') as JsonSchema | 'Failed to resolve';
}
function processNode(node: unknown, context: ResolverContext): unknown {
if (node === null || node === undefined) {
return node;
}
if (Array.isArray(node)) {
return node.map((item) => processNode(item, context));
}
if (typeof node !== 'object') {
return node;
}
const nodeObj = node as JsonSchema;
if (nodeObj['$ref'] && typeof nodeObj['$ref'] === 'string') {
const ref = nodeObj['$ref'];
if (context.resolvedReferences[ref]) {
// Already resolved
return { $ref: ref };
}
const resolvedSchema = resolveReference(ref, context.fullSchema);
if (!resolvedSchema) {
return { $ref: 'Failed to resolve' };
}
// Temporarily set to empty object to mark as being processed (prevents infinite recursion)
context.resolvedReferences[ref] = {} as JsonSchema;
const processedReference = processNode(resolvedSchema, context) as JsonSchema;
context.resolvedReferences[ref] = processedReference;
return { $ref: ref };
}
const result: JsonSchema = {};
for (const [key, value] of Object.entries(node)) {
result[key] = processNode(value, context);
}
return result;
}
/**
* Resolves all $ref references in a sub-schema using definitions from a full schema
*
* @param subSchema - The sub-schema to resolve references in
* @param fullSchema - The full schema containing all definitions
* @returns A new schema object with all references resolved and definitions included
*
* @example
* ```typescript
* const fullSchema = {
* definitions: {
* Person: { type: 'object', properties: { name: { type: 'string' } } },
* }
* };
*
* const subSchema = {
* type: 'object',
* properties: {
* company: { $ref: '#/definitions/Person' }
* }
* };
*
* const resolved = resolveSchemaReferences(subSchema, { fullSchema });
* // resolved is:
* {
* type: 'object',
* properties: {
* company: { $ref: '#/definitions/Person' }
* },
* references: {
* '#/definitions/Person': {
* type: 'object',
* properties: {
* name: { type: 'string' },
* }
* },
* }
* }
* ```
*/
export function resolveSchemaReferences(subSchema: JsonSchema, fullSchema: JsonSchema): JsonSchema {
// Initialize the resolver context
const context: ResolverContext = {
fullSchema,
resolvedReferences: {}
};
// Process the sub-schema to resolve all references
const resolvedSubSchema = processNode(subSchema, context) as JsonSchema;
// Build the final schema with the definitions block
const result: JsonSchema = {
...resolvedSubSchema
};
// Add the definitions block if any definitions were resolved
if (Object.keys(context.resolvedReferences).length > 0) {
result['references'] = context.resolvedReferences;
}
return result;
}