oscar 796a2bf89a fix(schema): stop asserting uri/iri format
The format keyword is annotation-only by default under JSON Schema, so
emitting a diagnostic for a malformed uri or iri was stricter than the
spec's default vocabulary. Asserting it also required parsing every value
as a URL, which is the only reason user-document validation reached for
full idna hostname handling. Treat uri/iri as annotations like every
other unrecognized format.
2026-07-04 01:26:01 +02:00

json-ls

A JSON/JSONC language server. One binary, speaking the Language Server Protocol over stdio.

Features

  • Diagnostics. Parse errors from the error-tolerant parser, plus validation findings from any bound JSON Schema, anchored to the offending node.
  • Completion. Schema-aware proposals for property names and for enum, const, and boolean values.
  • Hover. Titles, descriptions, types, and constraints from the schema at the cursor.
  • Go to definition. On an internal $ref (#/...), jumps to the node the JSON Pointer names in the same document. On a property key in a schema-bound document, jumps to where the schema declares that property.
  • Document symbols. An outline of the object and array structure.
  • Formatting. Re-emits the syntax tree, preserving comments.
  • JSONC. Line and block comments and trailing commas are part of the syntax, not an error. The document's languageId decides how strictly they are reported: documents opened as json get a warning for each comment and trailing comma, while jsonc documents (and any other language id) accept them silently. Editors set the languageId from their own file associations, so .json files are held to strict JSON and .jsonc files are not, with no server configuration involved.

Building

Requires a recent stable Rust toolchain (edition 2024).

cargo build --release

The binary lands at target/release/json-ls. It takes no arguments and communicates over stdin/stdout, so point your editor's LSP client at it.

Schemas

A document is bound to a schema through its $schema member:

{
  "$schema": "https://example.com/schemas/config.json",
  "name": "hello"
}

Relative and file:// URLs resolve against the document. Workspace-local schema files load automatically. Remote schemas are served from the on-disk cache when present, and are otherwise offered as a code lens and code action. Opening or editing a document never touches the network: only accepting that download offer does, via the json-ls.downloadSchema command.

Downloads are guarded by default:

  • Plaintext http:// URLs are refused.
  • Hosts must resolve to public addresses. Loopback, private, and link-local ranges are refused, and connections are pinned to the vetted addresses.
  • Same-origin redirects are followed silently. A cross-origin redirect pauses the download and asks for confirmation in the editor.
  • Redirect chains are capped at 3 hops and bodies at 8 MiB.

Each guard can be relaxed through configuration. Downloaded schemas are cached under $XDG_CACHE_HOME/json-ls/schemas (or ~/.cache/json-ls/schemas).

What is supported

json-ls implements a deliberate subset of JSON Schema and of the Language Server Protocol. This is the current inventory.

JSON Schema

Validation honors these keywords:

  • Any value: type (including integer and type arrays), enum, const
  • Numbers: minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf
  • Strings: minLength, maxLength, pattern, format
  • Objects: properties, patternProperties, additionalProperties, required
  • Arrays: prefixItems, items
  • Combinators: allOf, anyOf, oneOf, not, if/then/else
  • References: $ref to pointers within the same schema (#/...)

Completion and hover read from the same subset, plus the annotation keywords title, description, default, examples, deprecated, readOnly, and writeOnly.

The boundaries to be aware of:

  • Anything not listed is ignored. Notably absent today: uniqueItems, minItems/maxItems, minProperties/maxProperties, contains, propertyNames, dependentRequired/dependentSchemas, and the unevaluated* keywords.
  • An external or cross-document $ref is not followed. Validation skips it and emits a warning diagnostic at that location, so the missing checks are visible rather than silent.
  • format violations are warnings, not errors. Checked formats are date, time, date-time, email, ipv4, uuid, uri, and iri. Unknown formats are treated as annotations.
  • pattern and patternProperties use ECMA-262 regex syntax. A construct the regex engine cannot compile disables that one check instead of failing validation.
  • No specific draft is targeted, and dialect declarations inside a schema document are not interpreted.

Language Server Protocol

  • Lifecycle: initialize (UTF-8 or UTF-16 position encoding negotiation), initialized, shutdown, exit
  • Text sync: textDocument/didOpen, didChange (incremental), didClose
  • textDocument/publishDiagnostics (push model)
  • textDocument/completion, snippet-based. A client without snippet support gets no proposals. Item documentation is built on demand through completionItem/resolve.
  • textDocument/hover
  • textDocument/definition, resolving an internal $ref pointer against the document's own tree, or a property key against the bound schema's source (a remote schema resolves to its cached file so the target is openable)
  • textDocument/documentSymbol
  • textDocument/formatting
  • textDocument/codeLens, textDocument/codeAction, and workspace/codeLens/refresh for the schema download offer
  • workspace/executeCommand with the json-ls.downloadSchema command
  • workspace/didChangeConfiguration and workspace/configuration, as described under Configuration
  • window/showMessage and window/showMessageRequest

Unknown requests receive a method-not-found error and unknown notifications are ignored. There is no dynamic capability registration and no pull diagnostics.

Configuration

All settings live under a schemaFetch object:

Setting Type Default Effect
allowInsecureHttp boolean false Permit plaintext http:// schema URLs.
allowPrivateNetwork boolean false Permit hosts that resolve to loopback, private, or link-local addresses.
redirects string "prompt" Cross-origin redirect handling: "prompt", "same-origin" (refuse), or "always" (follow silently).
maxRedirects number 3 Redirect chain budget. 0 disables redirect following.
maxBytes number 8388608 Maximum schema body size in bytes. 0 means no limit.

The server reads them from three places:

  1. initializationOptions at startup.
  2. workspace/didChangeConfiguration notifications, under a json-ls section (a payload that is the bare settings object also works).
  3. A workspace/configuration pull for the json-ls section, used when the client advertises support and a change notification carries no usable payload.

Runtime settings layer over the initializationOptions base, so removing a setting at runtime reverts it to its startup value.

Editor setup

Neovim (0.11+)

vim.lsp.config['json-ls'] = {
  cmd = { 'json-ls' },
  filetypes = { 'json', 'jsonc' },
  settings = {
    ['json-ls'] = {
      schemaFetch = {
        redirects = 'same-origin',
        maxRedirects = 1,
      },
    },
  },
}
vim.lsp.enable('json-ls')

Development

cargo build
cargo test
cargo clippy --all-targets
cargo +nightly fmt    # the rustfmt config uses unstable options

Tests live inline as #[cfg(test)] modules. The end-to-end tests in src/integration_tests.rs drive the server dispatcher directly, without real stdio. stdout is reserved for protocol traffic, and all logging goes to stderr.

License

Apache-2.0, see LICENSE for more information.

S
Description
No description provided
Readme Apache-2.0
1.2 MiB
Languages
Rust 100%