Skip to content

Preserve sibling object keywords in schema previews - #1985

Open
gjermundgaraba wants to merge 1 commit into
UsefulSoftwareCo:mainfrom
gjermundgaraba:fix/schema-preview-compositions
Open

Preserve sibling object keywords in schema previews#1985
gjermundgaraba wants to merge 1 commit into
UsefulSoftwareCo:mainfrom
gjermundgaraba:fix/schema-preview-compositions

Conversation

@gjermundgaraba

@gjermundgaraba gjermundgaraba commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Inline schemas with properties beside allOf, anyOf, or oneOf lost those properties, and their required keys, in TypeScript tool previews; only $id-named definitions kept them. Found through a connected MCP server whose create tool requires metadata.type: tools.describe.tool() showed metadata as { [k: string]: unknown }, so agents omitted type and failed the server's runtime validation.

// { type: "object", properties: { type: { type: "string" } }, required: ["type"],
//   allOf: [{ type: "object", additionalProperties: {} }] }
before: { [k: string]: unknown; }
after:  ({ [k: string]: unknown; } & { type: string; })

The vendored compiler is based on upstream json-schema-to-typescript 15.0.4. Upstream fixed the same bug in bcherny/json-schema-to-typescript#708, released in 16.0.0, and added the same intersection re-entry guard in bcherny/json-schema-to-typescript#829, which is still unreleased. This is the minimal equivalent, written before I found the upstream change: named definitions already joined the composition through NAMED_SCHEMA, and inline schemas now take the same path. For the schemas in the new tests, the output matches 16.0.0's, with one exception: this branch names a titled definition after the whole composition, where 16.0.0 still splits it into Metadata and Metadata1. The parser also no longer crashes to unknown when a composed definition's branch refers back to it.

Not in this PR: a pattern-only string allOf still renders as ({ [k: string]: unknown; } & string), and a required key with no matching properties entry is still dropped. Both are separate, pre-existing issues; upstream 16.0.0 renders the first as string.

Verification

  • bun run format:check
  • bun run lint
  • bun run typecheck
  • bun run test
  • e2e — scenarios/tool-descriptions.test.ts on selfhost; artifact e2e/runs/selfhost/tools-agent-visible-descriptions-snapshot/descriptions.md shows metadata?: ({ [k: string]: unknown; } & { type: string; }).

The Cloudflare and Vercel real-spec snapshots are unchanged, and the Stripe fixture assertions still pass. Known limitation, documented in the vendored README: a null branch of an anyOf/oneOf beside sibling properties is absorbed by the intersection, as named definitions already did and as upstream 16.0.0 renders it.

Checklist

  • Added a changeset.
  • Added or updated tests for the new behaviour.
  • No secrets, credentials, or private data in the diff.

Inline schemas with properties or required beside allOf, anyOf, or oneOf
lost those siblings in compiled TypeScript previews; only $id-named
definitions kept them. Match inline schemas as UNNAMED_SCHEMA so the
parser builds the same intersection, name a titled definition after the
whole composition rather than its sibling fragment, and reuse the cached
intersection when a recursive reference re-enters it instead of crashing
the preview to unknown.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant