Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Cli Json Lint Laravel Package

owlcorp/cli-json-lint

A command-line JSON linter for PHP and Laravel projects that validates JSON files, catches syntax errors, and reports clear, actionable output. Add it to Composer scripts, local workflows, or CI pipelines to keep configuration files reliable and consistently checked.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a CLI binary, not a Laravel package. There is no service provider, no config file to publish, no facade, and no container binding. Don't spend time looking for config/json-lint.php or an Artisan command — you invoke it as a process, the same way you'd invoke phpunit or php-cs-fixer.

Minimal setup:

composer require --dev owlcorp/cli-json-lint

Then find the binary Composer actually installed. The bin entry is the source of truth, not the README:

composer show owlcorp/cli-json-lint          # note the version/tag you got
ls vendor/bin | grep -i json                 # confirm the executable name
vendor/bin/<binary> --help                   # read the real flags, output format, exit codes

Read --help first, the README second — on a low-star package the help output is usually the most current documentation, and the README tends to lag the flags.

First use case: lint the two JSON surfaces that actually break a Laravel app.

  1. lang/*.json — JSON translation files are the classic footgun. They get edited by translators, by export tooling, or by hand, and a single trailing comma or smart quote makes __('key') throw or silently fall back at runtime. Nothing in Laravel validates these for you.
  2. composer.json — a broken one breaks installs and CI for the whole team.
vendor/bin/<binary> lang composer.json
echo $?          # confirm it's non-zero on failure before you wire it into anything

That's the whole smoke test. If the exit code behaviour is right, everything else is plumbing.

Implementation Patterns

Pattern 1 — Composer script, with a guard. Convenient, but see the --no-dev gotcha below. Keep it out of install lifecycle hooks; make it an explicit target:

"scripts": {
  "lint:json": "vendor/bin/<binary> lang composer.json"
}

For a real file list, a shell script beats a Composer script — Composer scripts handle argument passing and globs badly.

Pattern 2 — Makefile / Taskfile target. This is the pattern I'd actually recommend, because it gives you the swap-in-one-line escape hatch:

lint-json:
	@git ls-files -z '*.json' | grep -zv -E '^(vendor|node_modules)/' | xargs -0 vendor/bin/<binary>

lint-json-staged:
	@git diff --cached --name-only --diff-filter=ACM -z -- '*.json' \
	  | grep -zv -E '^(vendor|node_modules)/' \
	  | xargs -0 -r vendor/bin/<binary>

Using git ls-files / git diff --cached rather than a directory walk is the important part: it gets you the ignore rules for free and dodges the vendor/node_modules problem entirely.

Pattern 3 — Pre-commit hook. Fast, local, catches the mistake before review:

#!/bin/sh
set -e
vendor/bin/<binary> $(git diff --cached --name-only --diff-filter=ACM -- '*.json')

Note the unquoted expansion — fine for paths without spaces, broken for paths with them. Use the -z/xargs -0 variant above if your repo has any. If you already run CaptainHook or GrumPHP (common in Laravel codebases), register this as a task there rather than hand-rolling .git/hooks/, so the hook is versioned and shareable.

Pattern 4 — CI gate. One step, non-zero exit does the work:

- run: composer install --no-interaction --prefer-dist
- run: vendor/bin/<binary> lang composer.json

For monorepos or large translation sets, lint only changed files on PRs and the full set on main:

- run: |
    files=$(git diff --name-only origin/main... -- '*.json' | grep -v -E '^(vendor|node_modules)/' || true)
    [ -z "$files" ] || vendor/bin/<binary> $files

Pattern 5 — the build-it-yourself alternative (worth knowing before you adopt). In a Laravel app you can cover the highest-value case in a Pest test with zero dependencies:

it('ships valid JSON translation files', function () {
    foreach (File::glob(lang_path('*.json')) as $file) {
        expect(fn () => json_decode(File::get($file), true, 512, JSON_THROW_ON_ERROR))
            ->not->toThrow(JsonException::class);
    }
});

If that's all you need, this package doesn't earn its place. It earns it when you want a shell-level tool that also covers composer.json, seeders, fixtures, Postman collections, and OpenAPI specs, with batch traversal and clean error output, without writing per-directory tests.

Where it fits vs. where it doesn't:

Good fit Wrong tool
Pre-commit/CI syntax gate on *.json Schema/semantic validation (use composer validate, an OpenAPI validator)
lang/*.json regression guard JSONC files (.vscode/, tsconfig.json)
Fixture/seeder JSON in tests Anything you'd need to call from PHP at runtime

Gotchas and Tips

  • composer install --no-dev deletes the binary. Never wire this into post-install-cmd or post-autoload-dump — production and CI-with---no-dev will hard-fail. Explicit composer lint:json only.
  • Strict JSON means JSONC fails. .vscode/settings.json, tsconfig.json, devcontainer.json, and some .eslintrc.json files legitimately allow comments and trailing commas. Whitelist the directories you care about (lang, database, tests/Fixtures) instead of linting "all JSON in the repo" — otherwise you'll get noise you can't fix and the check gets disabled within a week.
  • Vendor and fixture files. Test fixtures frequently contain intentionally malformed JSON. A repo-root invocation will surface those as failures. Always drive the file list from git ls-files or an explicit path list.
  • Empty globs. In bash, vendor/bin/<binary> lang/*.json with no matches passes the literal string lang/*.json to the binary, producing a confusing "file not found" instead of a clean skip. Use nullglob, or better, git ls-files -z.
  • xargs swallows the exit code. It returns 123 if any child invocation fails, which is non-zero but not the tool's own code. If your CI logic inspects specific exit codes, call the binary directly rather than through xargs.
  • BOM and encoding. Some translation exports and older Windows editors prepend a UTF-8 BOM, which some strict parsers reject. If lang/en.json fails but looks fine in your editor, check with xxd file.json | head -1 for efbbbf.
  • Scope is syntax only. This is not JSON Schema validation. It confirms the file parses; it won't tell you a key is misspelled, a composer.json constraint is invalid, or an OpenAPI document is structurally wrong. Don't let it become the reason nobody runs the tools that do.
  • Typical failure causes, in order: trailing comma before } or ], an unescaped " inside a translated string, a smart quote (" ") pasted from Word or a doc, and a merge conflict marker left in the file. All four are one-line fixes — run the tool on the single failing file to get a line number rather than re-linting the batch.
  • Extension points are shell-level. There's no plugin API. Your extension surface is the wrapper script, the hook runner (CaptainHook/GrumPHP), and CI config — which is fine, and is exactly why the Makefile-target pattern above matters.
  • Pin the version and keep the escape hatch real. Low stars, a single-package vendor, and a narrow scope mean this is a convenience, not infrastructure. Pin it in require-dev, let composer.lock do its job, and keep the invocation behind one named target so swapping to jq empty, php -r 'json_decode(...)', or the Pest test above is a one-line change rather than a hunt through YAML.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core