Skip to content

Prefer local sibling schema resolution for relative $ref before remote $id lookup#1186

Merged
datho7561 merged 3 commits intoredhat-developer:mainfrom
shin19991207:chang-patch-1184
Feb 23, 2026
Merged

Prefer local sibling schema resolution for relative $ref before remote $id lookup#1186
datho7561 merged 3 commits intoredhat-developer:mainfrom
shin19991207:chang-patch-1184

Conversation

@shin19991207
Copy link
Member

What does this PR do?

Resolves relative $ref using local sibling schema path before remote $id ref

What issues does this PR fix or reference?

Fixes #1184

Is it tested? How?

Manually and with automated tests.

{
  "$id": "https://example.com/schemas/primary.json",
  "type": "object",
  "properties": {
    "mode": {
      "$ref": "secondary.json"
    }
  },
  "required": ["mode"],
  "additionalProperties": false
}
{
  "$id": "https://example.com/schemas/secondary.json",
  "type": "string",
  "enum": ["dev", "prod"]
}
# yaml-language-server: $schema=./primary.json
mode: stage
# expect 'Value is not accepted. Valid values: "dev", "prod".' instead of 'Unable to load ...'

@datho7561
Copy link
Contributor

It's working okay, but it seems like the schema is cached somehow, so that if the second schema is changed, the changes aren't loaded properly until you close and reopen VS Code. Caching makes sense if it's a remote schema, but not really if it's local.

@datho7561
Copy link
Contributor

datho7561 commented Feb 19, 2026

I think it'd be helpful to add a test with an absolute reference as well, eg.

primary.json

{
  "$id": "https://example.com/schemas/primary.json",
  "$ref": "/schemas/secondary.json"
}

secondary.json (in the same folder as primary)

{
  "$schema": "https://json-schema.org/draft/2019-09/schema",
  "$id": "https://example.com/schemas/secondary.json",
  "type": "object",
  "properties": {
    "name": { "type": "string" },
    "age": { "type": "integer", "minimum": 0 }
  },
  "required": ["name", "age"]
}

@DorianCzichotzki
Copy link

Will this break bundled schemas? Or is that resolution done even earlier.

A bundled version of the example would be:

{
  "$id": "https://example.com/schemas/primary.json",
  "type": "object",
  "properties": {
    "mode": {
      "$ref": "secondary.json"
    }
  },
  "required": ["mode"],
  "additionalProperties": false,
  "$defs": {
    "https://example.com/schemas/secondary.json": {
      "$id": "https://example.com/schemas/secondary.json",
      "type": "string",
      "enum": ["dev", "prod"]
    }
  }
}

In this case the embedded schema should be used.

@shin19991207
Copy link
Member Author

@datho7561 I revisited the 'local schema changes not reloaded' issue with the code and thought of a better approach. When resolving $id, try a local sibling file first: _preferLocalBaseForRemoteId() builds a file URI from the current schema’s directory + the $id filename, uses it if it can be loaded; otherwise falls back to $id resolution.

I added test cases for reloading local schema after changes & absolute reference as well.

@DorianCzichotzki Bundled schemas should not be affected. They are resolved before any external fetch. In yamlSchemaService, we index embedded resources first then resolve $ref against that index.

@datho7561
Copy link
Contributor

It seems like the example in #1186 (comment) doesn't work properly from my testing.

@shin19991207
Copy link
Member Author

Okay. I reviewed and updated how schema resolution should work when attempting to load schemas from a local file before falling back to remote URIs. It comes in two cases:

1. Relative reference

Given:
/some/path/schemas/primary.json

{
  "$id": "https://example.com/schemas/primary.json",
  "$ref": "secondary.json"
}

/some/path/schemas/secondary.json

{
  "$id": "https://example.com/schemas/secondary.json",
  "type": "string"
}

YAML file:

# yaml-language-server: $schema=/some/path/schemas/primary.json
...

Here, "$ref": "secondary.json" first resolve relative to the retrieval URI of primary.json (/some/path/schemas/primary.json), producing /some/path/schemas/secondary.json.

If that local file is missing, the next attempt is against the $id field (https://example.com/schemas/primary.json) as https://example.com/schemas/secondary.json.

2. Absolute reference

Given:
/some/path/schemas/primary.json

{
  "$id": "https://example.com/schemas/primary.json",
  "$ref": "/schemas/secondary.json"
}

/some/path/schemas/secondary.json

{
  "$schema": "https://json-schema.org/draft/2019-09/schema",
  "$id": "https://example.com/schemas/secondary.json",
  "type": "object",
  "properties": {
    "name": { "type": "string" },
    "age": { "type": "integer", "minimum": 0 }
  },
  "required": ["name", "age"]
}

Under standard JSON Schema rules, an absolute JSON Pointer-like path resolves against the base URI, not the local filesystem. Since primary.json has $id = https://example.com/schemas/primary.json, resolving the absolute reference "/schemas/secondary.json" against that produces https://example.com/schemas/secondary.json.

From there, the relative path secondary.json is tried locally to /some/path/schemas/secondary.json as first attempt to resolve (this avoids interpreting /schemas/secondary.json as a filesystem path such as file:///schemas/secondary.json).

Second attempt falls back to https://example.com/schemas/secondary.json.

…$id ref

Signed-off-by: Morgan Chang <shin19991207@gmail.com>
… reloaded' issue

Signed-off-by: Morgan Chang <shin19991207@gmail.com>
Signed-off-by: Morgan Chang <shin19991207@gmail.com>
Copy link
Contributor

@datho7561 datho7561 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems to be working as described in the spec now. Thanks, Morgan!

@datho7561 datho7561 merged commit d3774ae into redhat-developer:main Feb 23, 2026
4 checks passed
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.

Support resolution of local schemas

3 participants