File

libs/core/mcp/src/utils/json-schema-ref-resolver.ts

Description

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

Indexable

[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;
}

results matching ""

    No results matching ""