Skip to content

Resolve external $ref URLs with a fetchSchema hook - #178

Open
samlown wants to merge 6 commits into
jsonnext:mainfrom
invopop:upstream-pr/fetch-schema-refs
Open

samlown wants to merge 6 commits into
jsonnext:mainfrom
invopop:upstream-pr/fetch-schema-refs

Conversation

@samlown

@samlown samlown commented Oct 27, 2026 •

Copy link
Copy Markdown

Builds on #176 and #177. Only the last commit (f286279) is new here. Once those are merged, this diff will shrink to that commit.

What

Schemas often split their definitions across documents with "$ref": "https://…". Until now those references were left unresolved, so completion, hover and validation stopped at them. This adds an opt-in fetchSchema hook:

jsonSchema({
  schema,
  fetchSchema: async (url) => {
    const res = await fetch(url);
    return res.ok ? res.json() : undefined;
  },
});

The same options work for json5Schema and yamlSchema. Passing a schema directly, as in jsonSchema(schema), still works as before, and so does { schema } without a hook.

How

  • Immediate fallback: a view plugin compiles the schema straight away, so features work without waiting for the network.
  • Resolution: RefResolver collects every external $ref from all schema-bearing keywords, fetches the base URLs (fragment removed) in parallel, and follows transitive refs. It caches and deduplicates requests, guards against cycles, and registers each schema with addRemoteSchema.
  • Use in features: the resolved node is kept in editor state and used by completion, hover and validation. The linter refreshes when it changes.
  • Safety: if every remote schema is already available, resolution can finish before the deferred fallback. The fallback then doesn't replace the resolved node. Nothing is dispatched once the editor is destroyed.
  • Exports: RefResolver and collectExternalRefs are exported. RefResolver applies the same $ref sibling normalization as the editor state.
  • Dev page: a new "dynamic ($schema)" mode loads the schema named in the document's $schema.
  • README: documents the hook and fixes a few existing examples.

Tests

  • Coverage:
    • The resolver: keyword coverage, fetch failures, deduplication, caching, cycles, and constraints from fetched schemas enforced end to end.
    • The plugin: resolved node kept when fetching finishes first, and no dispatch after destroy.
    • Options detection for all three modes.
  • Results: pnpm test (180 tests), pnpm tsc and pnpm build pass.

🤖 Generated with Claude Code

samlown and others added 2 commits October 27, 2026 14:36
Move completion, hover and validation to the v10 compileSchema API.
v10 keeps sibling keywords next to $ref, so a $ref combined with a
oneOf of consts now offers those values as completions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
json-schema-library validates "if" against a copy of the validation
path so the nodes it evaluates aren't recorded. The path holds every
node visited so far and is never trimmed, so each "if" costs time
proportional to everything validated before it and validation is
quadratic in document size. Schemas that select definitions with
if/then took ~30s on a 380KB document and crashed the tab at 2MB.
v10 and v11 are both affected.

Compile schemas with drafts whose "if" validator uses the shared path
and truncates it afterwards, which discards the same entries without
the copy and keeps the dynamic scope for $dynamicRef and $recursiveRef.
All features now compile through utils/compile-schema.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f286279

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
codemirror-json-schema Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@netlify

netlify Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for codemirror-json-schema ready!

Name Link
🔨 Latest commit f286279
🔍 Latest deploy log https://app.netlify.com/projects/codemirror-json-schema/deploys/6ae0bbf8f2987d9c980bc381
😎 Deploy Preview https://deploy-preview-178--codemirror-json-schema.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

samlown and others added 4 commits October 27, 2026 14:56
json-schema-library v10 validators return errors only, and its
unevaluatedProperties re-runs "if" to find the evaluated branch, so the
wrapper's results match the library's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
json-schema-library resolves a $ref by replacing the node with its
target and dropping sibling keywords, the draft 4/7 behaviour, even when
the schema declares draft 2020-12. That discards constructs like

  { "$ref": "#/$defs/key", "oneOf": [ { "const": "..." }, ... ] }

where the oneOf carries the real constraint, so no value completions
are offered and the constraint isn't validated.

Normalize schemas on entry into the equivalent, draft-agnostic
{ "allOf": [ { "$ref": ... }, { ...siblings } ] }. Annotation keywords
(title, description, ...) stay at the top level so property metadata is
preserved. This replaces the getRawSchemaForPointer workaround, which
only followed object properties and broke on a root-level $ref and on
array item pointers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Drafts 04, 06 and 07 ignore keywords next to $ref, so rewriting them
into an allOf changed what those schemas accept. Skip schemas, and
nested resources, whose $schema declares one of those drafts, matching
how json-schema-library picks a draft. Schemas without $schema keep
being normalized, as the library treats them as 2020-12.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The bundled extensions accept { schema, fetchSchema }. A view plugin
compiles the schema straight away so features keep working, then
RefResolver collects every external $ref, fetches the base URLs in
parallel, follows transitive refs, caches and deduplicates requests,
guards against cycles and registers each schema with addRemoteSchema.
The resolved node is kept in editor state and used by completion,
hover and validation; the linter refreshes when it changes.

The dev page gets a dynamic mode that loads the schema named in the
document's "$schema".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@samlown
samlown force-pushed the upstream-pr/fetch-schema-refs branch from 331f886 to f286279 Compare October 27, 2026 14:57
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