jsonvetdocsjsonvet reference

playgroundthis page on GitHub

jsonvet reference

The details behind the README. Why things work this way is in design.md.

Options

--dialect DIALECT[:GLOB]   check files matching GLOB (every file, without one) as DIALECT,
                           or as `auto`, the extension's default; the last match wins
--strict GLOB              the same as --dialect json:GLOB, kept from check-json5;
                           --strict '!GLOB' means --dialect auto:GLOB
--error RULE, --warn RULE, --ignore RULE
--max-warnings N
--fix                      fix what has a safe fix, in place, then report what is left
--unsafe-fixes             with --fix, also drop duplicate keys
--format text|sarif|json   text lines (the default), one SARIF 2.1.0 document, or one JSON
                           document on stdout
--color auto|always|never  color text output; auto colors it on a terminal unless NO_COLOR
                           is set, and anywhere when FORCE_COLOR is
--schema FILE              check each file against the JSON Schema in FILE, instead of the
                           schemas from settings or a file's own local $schema
--schema-catalog           enable HTTPS SchemaStore lookup by file name as a fallback
--stdin-filename PATH      the name standard input (`-`) goes by
--config FILE, --no-config
jsonvet fmt FILE...       format files instead of checking them, see [formatting](format.md)
jsonvet lsp               run the language server for editors, see [editors](editors.md)
--list-rules               list the rules with their default severities
--explain RULE             explain a rule, with an example

jsonvet --help prints the same list with more detail. jsonvet fmt also takes --check, which lists the files that would change and writes nothing (formatting).

Fixing

--fix rewrites each file with the fixes that are safe, then checks the result and reports what is left. The exit status comes from those problems, so a run that fixes everything exits 0. When it fixed something, it prints a count to standard error, such as fixed 3 problems in 2 files. With standard input and --format json or sarif the report takes standard error, so the count is left out.

Problem Fix Safe
byte-order-mark remove the mark yes
a trailing comma where the dialect forbids it (syntax) remove the comma yes
missing-final-newline add the newline yes
mixed-line-endings use the first line ending yes
duplicate-key keep the last, drop the rest no

Rules:

jsonvet writes a temporary file in the same folder, gives it the file's permissions, and renames it over the file, so a crash leaves the old text or the new one. A symlink is followed and stays a link. A read-only file is not changed and fails the run with an error. The new file keeps the mode but not the owner or extended attributes of the old one, and the rename replaces the file, so other hard links to it keep the old content.

With standard input, --fix prints the fixed text to standard output and the problems left to standard error, so jsonvet --fix --stdin-filename x.json < x.json > fixed.json works. A text it cannot fix comes back as it was.

In a pre-commit or prek hook, add args: [--fix]. The hook changes the files and fails that run, so you review the change and commit again.

- repo: https://github.com/simonvanlierde/jsonvet
  rev: v3.4.0
  hooks:
    - id: jsonvet
      args: [--fix]

Folders

A folder on the command line stands for the files under it with an extension or name from the README's dialect table, so jsonvet . does not check README.md. The walk works as ripgrep's does:

A file or folder you name is always checked, even when an ignore file or exclude names it. So exclude has no effect on the hook or the Action, which name each file: use the hook's own exclude: and the Action's files input there.

The arguments can reach a file more than once: as a repeated name, as a folder and a file in it, or as a symlink. jsonvet checks it once, under the name it was first given by.

Output

Each problem is one line: file:line:col: severity[rule] message. On a terminal, each problem shows as a code frame instead, with its line of the file and any related place marked, and a last line counts them. After 1,000 frames in a run, a line says so and the other problems show one line each. pre-commit runs hooks under a terminal when its color is on, so a commit made from a terminal shows frames. CI, pre-commit.ci and pipes get one line per problem, and | cat forces lines.

Frames are in color unless NO_COLOR is set or you pass --color never. --color always or FORCE_COLOR also color the severity and rule of piped lines. FORCE_COLOR=0 or false turns color off, even on a terminal. NO_COLOR wins over FORCE_COLOR, and --color wins over both.

Text output is in the language LC_ALL, LC_MESSAGES or LANG asks for, the first one set, when jsonvet has a translation for it, and in English otherwise. English is the only language so far. LANGUAGE and the Windows language setting are not read. LC_ALL=C gives English, and so do --format json and --format sarif, whose documents never change with the locale. Rule ids, severities, JSONPaths and exit codes are never translated.

jsonvet escapes control, bidirectional and invisible characters in file names and messages, so no output line can drive a terminal or pass for a GitHub workflow command (the exact forms). A file name that is not UTF-8 shows with U+FFFD in text, JSON and annotations, and exactly in SARIF.

Exit status and files

Exit When
0 no file has an error, or there were no files
1 a file has an error, or a file cannot be read
2 a usage error, such as an unknown option or a bad schema
101 a bug in jsonvet: it panicked

On a panic, jsonvet prints the message and where it happened to standard error, with a link to the issue tracker, and exits 101. Output not yet written is lost, and the verdict for the files checked so far is not given.

On exit 2 or 101 the run may stop part way, so a SARIF or JSON document on standard output may be cut off. Do not upload it.

jsonvet reads regular files, and follows a symlink to one. A device, FIFO or socket prints error: not a regular file. When the output pipe closes early, as in jsonvet … | head -1, jsonvet stops quietly, with the verdict for the files it has checked. Any other failed write to standard output or standard error, such as a full disk, ends the run with exit 2.

Standard input

- reads standard input, named - and checked as JSON5. --stdin-filename PATH names it PATH instead, in the output and for picking its dialect and settings, and makes the - optional. Empty input is a syntax error. To check a file named -, pass ./-. jsonvet reads standard input only when asked, so echo '{}' | jsonvet checks nothing and exits 0. With --fix, the fixed text goes to standard output (Fixing).

File names that look like options

pre-commit passes file names with no -- before them. So an unknown -x or --name, or a known option with a value such as --dialect=json, is read as a file name when a file of exactly that name exists. To be sure, put -- before the file names: jsonvet -- --x.json.

Dialects

Rules and severities

A severity is error, warning (or warn), or ignore (or off). An ignored rule is not checked at all. Warnings fail a run only when there are more than --max-warnings N. syntax, unreadable and schema stay errors whatever a setting says.

jsonvet --list-rules lists the rules and jsonvet --explain RULE prints one of the sections below. SARIF output links each rule to its section here.

syntax

The file is not valid in its dialect: a missing or extra comma, a comment in strict JSON, an unclosed string. jsonvet reports the first one and stops there, since what follows cannot be read reliably. --fix removes a trailing comma where the dialect forbids it. Always an error.

Bad (json):  {"a": 1,}
Good (json): {"a": 1}

duplicate-key

Most parsers keep the last copy of a repeated key and drop the others without a word, so a value is lost; RFC 8259 leaves the result undefined. The message gives the key's JSONPath and where its first copy is. --fix --unsafe-fixes keeps the last copy and drops the others. An error in every dialect.

Bad:  {"port": 80, "port": 8080}
Good: {"port": 8080}

invalid-number

I-JSON (RFC 7493) asks for numbers a double can hold, and for integers within 2^53 - 1 either side of zero, so that every parser reads them the same. jsonvet reports an overflow, and an integer written as digits alone past that limit. An error in i-json, off otherwise.

Bad (i-json):  {"id": 9007199254740993}
Good (i-json): {"id": "9007199254740993"}

invalid-code-point

I-JSON (RFC 7493) forbids lone surrogates and noncharacters in strings: many decoders reject them or replace them with U+FFFD. An error in i-json, off otherwise.

Bad (i-json):  {"a": "\ud800"}
Good (i-json): {"a": "\ud83d\ude00"}

byte-order-mark

RFC 8259 says JSON text must not start with a byte order mark (U+FEFF), and many parsers fail on one. Save the file as UTF-8 without a BOM. --fix removes it. An error in jsonl, a warning in json and i-json, off otherwise.

Bad:  <U+FEFF>{"a": 1}
Good: {"a": 1}

missing-final-newline

JSON Lines ends every record with a newline. Without one, the next record appended joins the last line and breaks it. --fix adds it. A warning in jsonl, off otherwise.

Bad (jsonl):  {"a": 1}<end of file>
Good (jsonl): {"a": 1}<newline>

mixed-line-endings

A file that uses more than one kind of line ending (LF, CRLF or a lone CR) shows whole files as changed in a diff, and tools split its lines differently. jsonvet reports the first ending that differs from the file's first one. --fix rewrites every ending to the first one. A warning in every dialect.

Bad:  {"a": 1,<CRLF>"b": 2,<LF>"c": 3}
Good: {"a": 1,<CRLF>"b": 2,<CRLF>"c": 3}

What counts as a line ending:

unused-suppression

In jsonc, jwcc and json5, a comment jsonvet-ignore RULE turns RULE off for the next line that starts with a token. Name more rules with commas, and give a reason after --. This rule reports a name in such a comment that hides nothing, is no rule, is a rule that is always an error (syntax, unreadable or schema), is a rule about the whole file (byte-order-mark, mixed-line-endings or missing-final-newline), or repeats a rule that an earlier name already gives for the same line. A name whose rule is off for the file is not reported. A suppression that names unused-suppression hides nothing, so it is reported too. A warning in jsonc, jwcc and json5, off otherwise.

Bad:  {"a": 1, // jsonvet-ignore duplicate-key<newline>"b": 2}
Good: {"a": 1,<newline>"b": 2}

How a suppression works:

unreadable

jsonvet could not check the file at all: it does not exist, is a device, FIFO or socket, could not be read, or is not UTF-8. It always fails the run, and no option changes that. Text output shows no rule for it; SARIF and JSON output file it under unreadable.

schema

A value the file's JSON Schema rejects, such as a missing required key or a string where the schema wants a number. The schema comes from --schema, the config file's schemas or a local $schema in the file, then falls back to SchemaStore when --schema-catalog is on. Always an error: --error, --warn, --ignore and settings files do not take this rule.

Globs

Configuration file

jsonvet reads jsonvet.toml, or a [tool.jsonvet] table in pyproject.toml. Each file takes the nearest one in its folder or a parent, which replaces any further up: nothing is merged. The search stops at the first folder with a .git entry, or without one at the working directory. A file outside the working directory takes only its own folder's. Standard input is looked up by its --stdin-filename, or from the working directory.

When the search finds nothing, jsonvet reads your own jsonvet/jsonvet.toml in $XDG_CONFIG_HOME (or ~/.config) on Linux and macOS, or %APPDATA% on Windows, with globs relative to the working directory. --config FILE reads that file for every file instead, and --no-config reads none. Options on the command line win.

A config file that is not a regular file is skipped, and one may hold at most 16 MB. An error in a symlinked config file names its line and column, but not the key or value. An unknown key is a usage error that names the closest known one.

[rules]
byte-order-mark = "error"

[[per-file-rules]]
files = "legacy/*.json"
duplicate-key = "ignore"

[[dialects]]
files = ".vscode/*.json"
dialect = "jwcc"

GitHub Action

All inputs are optional:

Input What it does
files paths or globs to check, separated by spaces; by default every tracked file the hook checks
args extra options, separated by spaces, such as --strict package.json
sarif also write a SARIF file to this path, besides the usual log and annotations
version the jsonvet version to install: X.Y.Z (also with a suffix such as rc1 or .post1), X.*, X.Y.* or latest
verify true (the default) checks the wheel's attestation before installing it; false skips that

In files, ** matches folders at any depth on every runner, and a glob matches files only. The Action skips symlinks, and by default submodules. With the default files, run actions/checkout first. With no JSON files, the step passes with a warning. Windows limits a command line to 32,767 characters, so with thousands of JSON files on a Windows runner, narrow files.

Do not pass --fix in args. The fixes stay on the runner and are not committed. The step can pass, and its log and the SARIF file report the files after fixing.

By default the Action installs the release that matches its own tag: @v3 gets the newest 3.x on PyPI, and @v3.0.0 gets 3.0.0. Pinned to a commit or a branch, it installs the release named in that commit's Cargo.toml, not the code in it. Before it installs the wheel, it checks that the wheel came from jsonvet's release.yml (how). On a runner without gh, or for a release before 3.1.0, it installs the wheel unchecked, with a notice. Set verify: false on GitHub Enterprise Server.

SARIF

--format sarif prints one SARIF 2.1.0 document instead of text lines:

jsonvet --format sarif $(git ls-files '*.json') > jsonvet.sarif

The exit status and --max-warnings work as with text, and no annotations are printed. On exit 2 or 101 the document may be cut off, so do not upload it.

Code scanning

The Action's sarif input writes the document for you to upload to GitHub code scanning:

jobs:
  jsonvet:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      security-events: write
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - uses: simonvanlierde/jsonvet@v3
        with:
          sarif: jsonvet.sarif
      # Upload when jsonvet found problems too: that is when the step fails.
      - uses: github/codeql-action/upload-sarif@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
        if: success() || failure()
        with:
          sarif_file: jsonvet.sarif

JSON

--format json prints one JSON document instead of text lines, for scripts and editors:

jsonvet --format json config.json5 | jq -r '.diagnostics[] | "\(.file):\(.line) \(.rule)"'
{"version":1,"diagnostics":[
{"file":"dup.json","rule":"duplicate-key","url":"https://github.com/simonvanlierde/jsonvet/blob/main/docs/usage.md#duplicate-key","severity":"error","message":"duplicate key $.a (first at 2:3)","line":3,"column":3,"offset":14,"related":[{"message":"first copy","line":2,"column":3,"offset":4}]},
{"file":"gone.json","rule":"unreadable","url":"https://github.com/simonvanlierde/jsonvet/blob/main/docs/usage.md#unreadable","severity":"error","message":"no such file","line":null,"column":null,"offset":null,"related":[]}
]}

SchemaStore catalog

jsonvet --schema-catalog bower.json enables HTTPS downloads from SchemaStore. Default runs and the language server stay offline. Repository settings cannot enable downloads.

--schema takes precedence, then a matching schemas entry in the configuration, then a document's local $schema declaration. The catalog is the fallback. A local declaration keeps precedence even if its file is missing. Document $schema URLs are never fetched. Catalog patterns without / match file names; others match the normalized path. A leading **/ also matches zero directories. --stdin-filename supplies the path for standard input. For JSON Lines, a matching schema checks each document separately.

The first matching entry wins. Downloads and redirects support only https://schemastore.org, https://www.schemastore.org and https://json.schemastore.org. A matched entry on another origin is a usage error. External $ref files and URLs remain unsupported, including in downloaded schemas; a matched schema that cannot be used is an error. For example, the current package.json schema references prettierrc.json and cannot be used through the catalog. Supply a self-contained schema with --schema for such files. No document text or local paths are sent to SchemaStore.

Catalogs and schemas stay fresh for seven days. If a refresh fails or returns unusable data, jsonvet uses a usable stale copy and warns on stderr. Without a usable copy, it stops with a usage error (exit 2). A download that cannot be written to the cache is still used, with a warning. Downloads allow three redirects and take at most 30 seconds each. After a timeout, an unknown host or a connection error, the rest of the run tries no more downloads and uses cached copies; their warnings say they were not tried. The catalog is limited to 4 MiB, 10,000 entries and 100,000 patterns; a schema is limited to 8 MiB. All schema validation limits below still apply.

The cache is under $XDG_CACHE_HOME/jsonvet/schemastore when XDG_CACHE_HOME is absolute. Otherwise it uses ~/.cache/jsonvet/schemastore on Linux, ~/Library/Caches/jsonvet/schemastore on macOS, or %LOCALAPPDATA%/jsonvet/schemastore on Windows. Each cache entry stores its URL, download time and body; the URL must match before reuse. Writes replace entries atomically after validation. Delete this directory to clear the cache.

JSON Schema limits

These hold for every schema: from --schema, a config file, a file's $schema or the catalog. Why each one exists is in design.md.

Coming from check-json5

The check-json5 package on PyPI keeps working through 3.x: it installs jsonvet. Besides the dialect change in the README, the output line, syntax-error wording, positions after a lone CR or U+2028, the hook's .jsonl and .ndjson files and the Python module changed (full list).