oscar 8c883cc56c fix(schema/store): unbind documents when they close
Nothing removed a closed document from the store, so doc_schema
entries and waiting-set URIs accumulated for the server's lifetime and
a completed fetch nominated long-closed documents for diagnostics.
2026-07-03 01:49:35 +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.
  • Document symbols. An outline of the object and array structure.
  • Formatting. Re-emits the syntax tree, preserving comments.
  • JSONC. Line and block comments are part of the syntax, not an error.

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/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%