The after-item comma-or-closer match in parse_object and parse_array was the same four arms duplicated, each also repeating its loop's own end-of-file close error.
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(includingintegerand 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:
$refto 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 theunevaluated*keywords. - An external or cross-document
$refis not followed. Validation skips it and emits a warning diagnostic at that location, so the missing checks are visible rather than silent. formatviolations are warnings, not errors. Checked formats aredate,time,date-time,email,ipv4,uuid,uri, andiri. Unknown formats are treated as annotations.patternandpatternPropertiesuse 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 throughcompletionItem/resolve.textDocument/hovertextDocument/documentSymboltextDocument/formattingtextDocument/codeLens,textDocument/codeAction, andworkspace/codeLens/refreshfor the schema download offerworkspace/executeCommandwith thejson-ls.downloadSchemacommandworkspace/didChangeConfigurationandworkspace/configuration, as described under Configurationwindow/showMessageandwindow/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:
initializationOptionsat startup.workspace/didChangeConfigurationnotifications, under ajson-lssection (a payload that is the bare settings object also works).- A
workspace/configurationpull for thejson-lssection, 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.