paperclipai/paperclip · error

Unresolved API schema: ${value.$ref}

Error message

Unresolved API schema: ${value.$ref}

What it means

buildRunnerApiCatalog dereferences internal $ref pointers ("#/...") in the OpenAPI document. When a $ref's JSON-pointer path does not resolve to a node in the document (after ~0/~1 unescaping), dereference throws "Unresolved API schema: <ref>". This is a build-time integrity check on the shipped API schema, not a caller-input error.

Source

Thrown at server/src/services/native-runtime/runner-api-catalog.ts:65

  if (/\/issues$/.test(path)) return method === "GET" ? ["search_tasks"] : ["create_task"];
  if (/\/issues\/\{[^}]+\}$/.test(path)) return method === "GET" ? ["get_task_context"] : ["set_dependencies", "finish_task", "block_task", "request_review"];
  if (/\/agents$/.test(path) && method === "GET") return ["list_agents"];
  if (/\/agents\/(me|\{[^}]+\})$/.test(path) && method === "GET") return ["get_agent"];
  if (/\/approvals$/.test(path) && method === "GET") return ["list_approvals"];
  if (/\/approvals\/\{[^}]+\}/.test(path) && method === "GET") return ["get_approval", "get_approval_context"];
  return [];
}

// Descriptions and schemas are documentation, never authorization. Routes remain
// authoritative, including conditional role, company and resource checks.
export function buildRunnerApiCatalog(document: Json = buildOpenApiDocument()): RunnerApiOperation[] {
  function dereference(value: any, seen = new Set<string>()): any {
    if (Array.isArray(value)) return value.map((entry) => dereference(entry, seen));
    if (!value || typeof value !== "object") return value;
    if (typeof value.$ref === "string" && value.$ref.startsWith("#/")) {
      if (seen.has(value.$ref)) return { description: `Recursive schema: ${value.$ref}` };
      const target = value.$ref.slice(2).split("/").reduce((node: any, key: string) => node?.[key.replace(/~1/g, "/").replace(/~0/g, "~")], document);
      if (!target) throw new Error(`Unresolved API schema: ${value.$ref}`);
      return dereference(target, new Set([...seen, value.$ref]));
    }
    return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, dereference(entry, seen)]));
  }
  const result: RunnerApiOperation[] = [];
  for (const [path, item] of Object.entries<Json>(document.paths)) {
    for (const [verb, operation] of Object.entries<Json>(item)) {
      if (!METHODS.has(verb)) continue;
      const method = verb.toUpperCase();
      const restriction = runnerApiRestriction(method, path);
      const skillReference = runnerApiReference[`${method} ${path.replace(/\{[^}]+\}/g, "{}")}`];
      const protocol = !path.startsWith("/api/") || /\/(oauth|auth|runtime-tools|mcp|ws)(\/|$)/.test(path)
        || /\/(claude-login|login-sessions|start-authorization|finalize-oauth-access)(\/|$)/.test(path)
        || /event-stream|websocket/i.test(JSON.stringify(operation.responses));
      result.push({
        operationId: `${method} ${path}`, method, path,
        summary: operation.summary ?? `${method} ${path}`,
        description: operation.description ?? "",

View on GitHub (pinned to 01ad858492)

Solutions

  1. Fix the OpenAPI document so every $ref target exists at the pointed path (re-add or rename the missing schema).
  2. Regenerate the API catalog document from the authoritative spec source instead of hand-editing.
  3. Check $ref strings for JSON-pointer escaping: "~" must be ~0 and "/" must be ~1 inside tokens.
  4. Add/extend a catalog build test that dereferences all paths so the dangling ref is caught at build time.

Example fix

// before (document)
{ "components": { "schemas": { "Issue": { ... } } }, "paths": { "/issues/{id}": { "$ref": "#/components/schemas/IssueRef" } } }
// after
{ "components": { "schemas": { "Issue": { ... }, "IssueRef": { ... } } }, ... }
Defensive patterns

Strategy: try-catch

Validate before calling

function assertRefsResolve(doc: any) {
  const refs = JSON.stringify(doc).match(/"\$ref":\s*"#([^"]+)"/g) ?? [];
  for (const r of refs) {
    const ptr = JSON.parse("{" + r + "}").$ref.slice(1);
    const node = ptr.split("/").filter(Boolean).reduce((n: any, k: string) => n?.[k.replace(/~1/g, "/").replace(/~0/g, "~")], doc);
    if (!node) throw new Error(`Dangling $ref: #${ptr}`);
  }
}
// run over the OpenAPI document before feeding buildRunnerApiCatalog

Try / catch

try {
  const catalog = buildRunnerApiCatalog(document);
} catch (err) {
  if (err instanceof Error && err.message.startsWith("Unresolved API schema:")) {
    // log the $ref from err.message and fix the spec / regenerate the document
  } else throw err;
}

Prevention

When it happens

Trigger: The OpenAPI document bundled into runner-api-catalog contains a $ref like "#/components/schemas/Foo" while components.schemas.Foo is missing, renamed, or the pointer contains unescaped ~ or / characters.

Common situations: Editing or regenerating the API spec and dropping a schema; hand-editing document.paths and referencing a schema that no longer exists; codegen output drift between spec versions; copy-pasting an operation with refs into a truncated document.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10). Data as JSON: /api/errors/08ea86674bad159c. Report an issue: GitHub.