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

Json Works Laravel Package

johnstevenson/json-works

A PHP library for reading, editing, and writing JSON while preserving original formatting and whitespace. It provides a simple API for precise changes to JSON documents, making it ideal for configuration files and other human-maintained JSON that should stay readable.

Deep Wiki
Context7

Getting Started

Install

composer require johnstevenson/json-works

Pure PHP, no extensions. Only 26 stars and the last release was 2023-04-17, so treat it as a small, stable, low-maintenance library: pin the version and add your own regression tests around it.

Where to look first

  1. src/Document.php — the entire public surface: fromJson(), fromFile(), toJson(), toFile(), query(), add(), replace(), remove(), validate().
  2. The Format/formatter classes — this is where indentation, line endings and "what whitespace do we emit for a newly added node" live. Read these before you rely on byte-for-byte round-trips.
  3. tests/ — the round-trip tests are the best documentation of the intended fidelity guarantees (trailing newline, empty {}, tabs vs spaces, unicode escapes).

First use case: patch composer.json without reformatting it

use JohnStevenson\JsonWorks\Document;

$doc = new Document();
$doc->fromFile(base_path('composer.json'));

$doc->add('/scripts/post-install-cmd', ['@php artisan migrate --force']);
$doc->replace('/require/php', '^8.2');

$doc->toFile(base_path('composer.json'));

Everything you did not touch — key order, 4-space indent, LF endings, the trailing newline — survives. That round-trip is the whole reason to pick this package over json_decode() + json_encode().

A good first internal use: a php artisan config:set-json style command, or a Composer plugin that adds a script entry to the host project, where a diff should show one added line, not a whole-file reformat.

Implementation Patterns

Load → mutate → validate → atomic write

This is the canonical day-to-day flow. Because toFile() writes directly, wrap it yourself in Laravel:

use Illuminate\Support\Facades\File;
use JohnStevenson\JsonWorks\Document;

function patchJson(string $path, callable $mutator): void
{
    $doc = new Document();
    $doc->fromFile($path);

    $mutator($doc);

    $doc->validate(); // throws on invalid document

    $tmp = tempnam(dirname($path), 'json-');
    file_put_contents($tmp, $doc->toJson());
    rename($tmp, $path); // atomic on same filesystem
}

rename() over file_put_contents() into the original path protects you against a half-written composer.json if the process dies mid-write. Combine with File::get()/LockableFile if you expect concurrent writers.

Pointer-builder helper — escape every segment

Manually building pointers is where bugs come from. Centralise it:

function ptr(string ...$segments): string
{
    return implode('', array_map(
        fn ($s) => '/' . str_replace(['~', '/'], ['~0', '~1'], (string) $s),
        $segments
    ));
}

// composer package names contain "/" — this is required, not optional:
$doc->replace(ptr('require', 'laravel/framework'), '^11.0');
// => /require/laravel~1framework

Query-then-branch, rather than blind mutation

query() throws when the pointer does not resolve, so treat it as an existence probe:

try {
    $current = $doc->query('/extra/merge-plugin/include');
} catch (\Throwable $e) {
    $doc->add('/extra/merge-plugin/include', []);
}

If you're doing several conditional edits, a small has($doc, $pointer) wrapper around a try/catch keeps the command classes readable. Prefer reading the value once and doing in-memory comparison before deciding to call replace() — a no-op replace() still rewrites the node and may normalise subtle formatting inside that node.

Array append and array edits

- is the JSON Pointer append token:

$doc->add('/autoload/psr-4/App\\/-', 'database/seeders/');

Array indices are resolved against the document at call time. When removing multiple elements from the same array, iterate in descending index order or re-read the array between operations, otherwise your indices shift out from under you.

Build brand-new documents with explicit formatting

When the target file does not exist yet, start from an empty document, set the formatting you want, then add nodes:

$doc = new Document();
$doc->fromJson('{}');
$doc->setIndent('    ');        // verify the exact setter in your pinned version
$doc->setLineEndings("\n");
$doc->add('/require', ['php' => '^8.2']);
$doc->toJson();

Pick indentation and line endings that match the rest of the repo (composer.json, .editorconfig) so the first write doesn't create a lint diff.

Wrap it in a Laravel service

Inject a thin JsonPatcher with patch(), read(), validateFile() methods rather than sprinkling new Document() through commands. That gives you one place to add logging, backups (.bak copy before rename), and your own pointer helper — and one place to swap the library out if you need to fork it.

Testing pattern

The most valuable test is a fidelity test: load a representative fixture, call toJson(), assert the output is byte-identical to the input. Then add tests per mutation asserting the diff is what you expect — not just that decode-then-assert passes, which would hide reformatting.

Gotchas and Tips

  • No URL-fragment pointers. #/foo/bar is not accepted; only plain /foo/bar. Strip the # yourself if you're piping in a URI fragment.
  • Empty string is the root pointer. $doc->replace('', $newValue) replaces the entire document; query('') returns the whole tree.
  • - only works for arrays. Using it on an object is an error. Object additions always append at the end of the object; they never insert between existing keys.
  • Adding the first member to an empty object/array is a formatting edge case. The library has to invent indentation for {} → { ... }. Assert against fixtures with empty containers before trusting it on real files.
  • Duplicate keys. Hand-maintained JSON sometimes has them. Don't assume the parser tolerates it — run validate() on inputs you don't control and fail loudly rather than silently picking the last value.
  • Number fidelity. Very large integers and floats are the classic JSON library trap. If you patch a file containing IDs or big decimals, verify they come back as the same literal (1.0 vs 1, 12345678901234567890). Round-trip a fixture containing both before you rely on it.
  • Unicode and escapes are preserved as authored. \u00e9 and é in the source stay in their original form; don't expect the library to normalise them. Same for escaped slashes.
  • Line endings and trailing newline. Fidelity depends on what the formatter records from the input. A file with mixed CRLF/LF, or one authored on Windows and edited on Linux, is worth an explicit fixture test. Set setLineEndings() explicitly for files you create from scratch.
  • toFile() is not atomic and takes no lock. Wrap it (temp file + rename()), as in the pattern above, or you risk a truncated config file on a crashed process.
  • query() throws on a missing pointer. There is no null-on-miss mode. Always try/catch or pre-check; a swallowed exception inside a loop is a common source of "my edit silently didn't apply".
  • No dot-notation, no JSONPath, no wildcards. It's strict RFC 6901. $.. or require.* will not work — iterate in PHP and build pointers.
  • Escape / in keys. Composer package names are the everyday example: /require/laravel~1framework. Same for ~ itself (~0). Use the ptr() helper everywhere.
  • Validation error messages are descriptive — use them. Catch the package's exception (check the exact class name in src/ for your pinned version), and surface getMessage() to the user or your log; it tells you the offset and the problem, which is far better than json_decode()'s null.
  • Whole file in memory. Parsing keeps the original text plus a formatted node tree, so memory is roughly a multiple of file size. Fine for composer.json and app config; not for multi-hundred-MB JSON dumps.
  • Mutations are pointer-driven, not merge-driven. There is no deep-merge helper. For merging config trees, decode to PHP, merge, then replace() the affected subtree — or add children one by one if you need per-line diffs.
  • Order of operations matters. Replace values before removing their parents; when in doubt, do removals last and sort same-array removals by descending index.
  • Extension points are thin. There's no plugin/event system — the practical extension surface is the formatter (indentation/line-ending decisions) and your own wrapper service. If you need custom escaping or key sorting, you're modifying or forking, so keep your call sites behind JsonPatcher.
  • Low-maintenance signal. 26 stars, last release 2023, no recent activity: pin the exact version in composer.json, keep a couple of round-trip fixtures in your own test suite, and assume you may need to vendor a fork for PHP-version compatibility issues. The upside is that the format-preserving core is small and stable, so the risk is mostly "nobody will fix it for you", not "the API will churn".
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.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator