A linter for JSON, I-JSON, JSONC, HuJSON (JWCC), JSON5 and JSON Lines. It finds syntax errors and duplicate keys, and tells you the exact line and column:
config.json:4:5: error[duplicate-key] duplicate key $.server.port (first at 2:5)
package.json:2:14: error[syntax] missing `,`
events.jsonl:12:1: error[syntax] blank line; JSON Lines needs a value on every line
Try it in your browser: the playground checks the text you paste as you type. Nothing to install, and the text stays on your machine.
- Six dialects, one parser. Each file is checked in the dialect its extension implies.
- JSON Schema with positions. Each schema error points at its value's line and column. jsonvet never fetches anything from the network.
- Fits CI. A pre-commit or prek hook, a GitHub Action, a command or a Rust library, with text, SARIF or JSON output.
- Fast. It was check-json5, a Python hook, until 2.0 rewrote it in Rust. It is up to 1,395 times faster than check-json5 1.4.1 (figures).
Add this to .pre-commit-config.yaml (for pre-commit or
prek):
- repo: https://github.com/simonvanlierde/jsonvet
rev: v3.2.0
hooks:
- id: jsonvetIt checks files by name: .json, .jsonc, .json5, .jsonl, .ndjson, .geojson, .jsonld,
.hujson, .jwcc, .code-workspace, .code-snippets, .webmanifest and .babelrc.
Options go in args, such as args: [--strict, package.json] to hold package.json to strict
JSON. The hook installs a prebuilt binary from PyPI, so you need no Rust.
- uses: simonvanlierde/jsonvet@v3The step fails when a file has an error, and each problem shows up as an annotation on the pull request. Before it installs jsonvet from PyPI, it checks that the wheel came from this repository's release workflow. Its inputs are in the reference.
uvx jsonvet config/*.json # run it from PyPI without installing
uvx jsonvet . # check every JSON-like file under a folder
pip install jsonvet # install it as a Python package
cargo install jsonvet-cli # build it from source
cargo binstall jsonvet-cli # download the binary from the GitHub releaseEach GitHub release also has the binary
for Linux, macOS and Windows, with checksums and an SBOM
(verifying a release).
A folder stands for the JSON-like files under it, skipping hidden and ignored files, and - reads
standard input. jsonvet --help lists every option, and the
reference has the details.
use jsonvet::{check, Config, Dialect};
let report = check("{\"a\": 1, \"a\": 2}", &Config::new(Dialect::Json));
for problem in report.problems() {
let at = report.location(problem.offset());
println!("{}:{}: {}[{}] {problem}", at.line, at.column, problem.severity(), problem.rule());
}The API is on docs.rs.
| Dialect | Adds to JSON | Default for |
|---|---|---|
json |
nothing (RFC 8259) | none; choose it with --dialect or --strict |
i-json |
stricter content (RFC 7493) | none |
jsonc |
// and /* */ comments |
.jsonc, .code-workspace, .code-snippets |
jwcc |
comments and trailing commas (HuJSON) | .hujson, .jwcc |
json5 |
the JSON5 1.0.0 grammar | .json, .json5, .geojson, .jsonld, .webmanifest, .babelrc |
jsonl |
one JSON value per line (JSON Lines) | .jsonl, .ndjson |
Any other extension is checked as JSON5. .json defaults to JSON5 because most .json files that
fail as JSON are config files with comments. To check a file as strict JSON, use
--dialect json:package.json.
| Rule | Reports | Default |
|---|---|---|
syntax |
the first syntax error; checking stops there | error |
duplicate-key |
a key repeated in one object, with both positions and its path | error |
invalid-number |
a number a double cannot hold, or an integer past ±(2⁵³ − 1) | error in i-json, off elsewhere |
invalid-code-point |
a lone surrogate or a noncharacter in a string | error in i-json, off elsewhere |
byte-order-mark |
a byte order mark at the start of the file | warning in json and i-json, error in jsonl |
missing-final-newline |
a JSON Lines file that does not end with a newline | warning in jsonl |
Change a rule's severity with --error, --warn or --ignore. Warnings fail a run only past
--max-warnings N.
To keep settings out of the command line, put them in jsonvet.toml, or in a [tool.jsonvet]
table in pyproject.toml:
rules = { byte-order-mark = "ignore", missing-final-newline = "error" }
# Checked in order; the last matching entry wins.
dialects = [
{ files = ".vscode/*.json", dialect = "jsonc" },
{ files = "package.json", dialect = "json" },
]
# Skipped when jsonvet walks a folder; files named on the command line are still checked.
exclude = ["node_modules", "tests/fixtures/*"]Each file takes the nearest config file in its folder or a parent, up to the repository root, so each package in a monorepo can have its own. Options on the command line win. The reference says where jsonvet looks and how globs match.
--schema FILE checks each file against one JSON Schema, and reports each error at its value:
$ jsonvet --schema server.schema.json server.json5
server.json5:3:9: error[schema] "80" is not of type "integer" (at $.port)
server.json5:6:3: error[schema] Additional properties are not allowed ('debug' was unexpected) (at $.debug)
A config file can give files a schema, and so can a file's own $schema path:
schemas = [{ files = "config/server.json5", schema = "schemas/server.schema.json" }]{ $schema: "../schemas/server.schema.json", port: 8080 }Such a schema must stay inside the project. A $schema URL is skipped: jsonvet fetches nothing,
so every $ref must point inside the schema file. Schema errors are always errors. The rules for
schema paths and the limits are in the
reference.
--format sarif prints a SARIF 2.1.0 document
for GitHub code scanning. The Action's sarif input writes one for you to upload:
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.sarifBump rev to the newest tag. The check-json5 hook id, the check-json5 command and --strict
keep working through 3.x.
jsonvet gives the same pass or fail as 1.4.1, apart from a few
edge cases.
One change can fail a file that passed: .jsonc, .code-workspace and .code-snippets files
are now checked as JSONC, and .hujson and .jwcc files as JWCC. To keep the old verdict, add
--dialect json5:GLOB.
The tests run on Linux, macOS and Windows, with fuzzing, mutation testing and a comparison with check-json5 1.4.1. A conformance matrix runs the same files against 18 other parsers in four languages:
| dialect | files | share of files where each parser gives the expected verdict |
|---|---|---|
| JSON5 | 166 | js-json5 100%, jsonvet-json5 100%, rs-json5 100%, py-json5 99%, py-pyjson5 99%, rs-serde-json5 94%, go-json5 91% |
| JSONC | 9 | js-jsonc-parser 100%, jsonvet-jsonc 100%, go-tidwall-jsonc 89%, js-strip-json-comments 89%, rs-jsonc-parser 78% |
| JWCC (HuJSON) | 10 | jsonvet-jwcc 100%, go-hujson 90% |
| JSON Lines | 11 | jsonvet-jsonl 100%, py-jsonlines 82%, go-json-stream 64%, rs-serde-json-stream 64% |
| Strict JSON (baseline) | 129 | js-json-parse 100%, jsonvet-json 100%, rs-serde-json 98%, py-json 96%, py-jiter 95% |
Run on 2026-10-28. The full grid, and where each expected verdict comes from: docs/conformance.md.
- Reference, design and CHANGELOG.
- CONTRIBUTING.md, ROADMAP.md and GOVERNANCE.md.
Found a file jsonvet gets wrong? Please open an issue with the file and the command. If it crashes or hangs jsonvet, report it privately as SECURITY.md says.
To cite jsonvet, use the DOI 10.5281/zenodo.23461825 or GitHub's "Cite this repository" button.
I (Simon van Lierde) started this as a fork of check-json5 by Ben Mares, which is based on the pre-commit-hooks repository by Anthony Sottile and contributors. It is MIT-licensed, like both of them.