# 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). ```sh 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: ```json { "$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+) ```lua 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 ```sh 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](LICENSE) for more information.