psx/json
A lightweight PHP library for parsing and writing JSON. psx/json provides a simple, dependable API for converting between JSON strings and PHP values, making it useful for APIs, configuration files, and data exchange in PSX-based or standalone projects.
Install with Composer:
composer require psx/json
Psx\Json is a small, framework-agnostic library: it ships no Laravel service provider, facade, or config file. You use the classes directly. The two you will touch first are:
PSX\Json\Patch — applies RFC 6902 JSON Patch operations (add, remove, replace, move, copy, test) to a document.PSX\Json\Pointer — resolves RFC 6901 JSON Pointers (e.g. /items/0/name) against a document.Where to look first: the repository's tests/ directory is the most reliable documentation. The README is thin; the test cases show the exact expected input/output shapes for each operation.
First use case that earns its keep: taking a stored JSON document (a webhook payload, a settings blob, a saved form schema) and applying a client-supplied patch to it, rather than trusting the client to send the whole document back. That is the canonical JSON-Patch workflow and the reason to reach for this package.
use PSX\Json\Patch;
use PSX\Json\Pointer;
$document = json_decode($storedPayload); // stdClass / array tree
$pointer = new Pointer('/user/email');
$email = $pointer->evaluate($document); // "[email protected]"
$patch = new Patch(json_decode($request->input('patch')));
$patched = $patch->apply($document); // RFC 6902 applied
A sensible first milestone: wrap applying a patch behind a Form Request and a small service class, and store the result. Do that before exploring move/copy/test semantics.
API partial updates. The pattern most Laravel devs will want: PATCH /documents/{id} accepts an RFC 6902 array, you load the row, apply, persist. Guard with a validation rule that each element has op and path before you ever construct a Patch, so malformed payloads fail as 422s rather than library exceptions.
$rules = ['patch' => 'required|array', 'patch.*.op' => 'required|in:add,remove,replace,move,copy,test', 'patch.*.path' => 'required|string'];
Optimistic concurrency with the test op. JSON Patch is often used as a compare-and-swap primitive. If a client includes a test at /version with the value it last saw, a failed test throws and you map that to HTTP 409. This lets you avoid a separate version column check and keeps the whole negotiation in the patch payload.
Pointer for safe nested reads. Instead of chained data_get(), Pointer gives you a single grammar for arbitrary deep access, including array indices and the - append token in patches. Useful when field paths themselves come from config or user input:
$config = json_decode($definition->schema);
$type = (new Pointer('/properties/status/enum/0'))->evaluate($config);
Normalize decode direction once. Decide up front whether your documents are stdClass trees (json_decode($json)) or associative arrays (json_decode($json, true)). Mixing the two across a codebase produces subtle failures when a patch addresses an object key that your model has flattened into an array, or vice versa. Put a helper in a base service (decodeDocument, encodeDocument) and never call json_decode inline again.
Audit trail. JSON Patch is a natural fit for event sourcing of document edits: persist the patch array and the resulting hash, keep the before/after for review. Replaying patches is deterministic on the same library version, so you can rebuild a document's history without storing full snapshots.
Pair with a Form Request + Policy. Since there is no framework glue, the cleanest integration is a thin feature layer: PatchDocumentRequest for shape validation, DocumentPatchPolicy for authorization, and a DocumentPatcher service that owns the Patch construction. Keep the package out of controllers.
Patch::apply() throws on invalid operations and failed test checks. Wrap in try/catch and translate to domain exceptions; do not let RFC 6902 errors bubble out as 500s. A failed test is a client conflict, not a server fault."", not "/". "/" refers to a key that is literally the empty string. This trips up nearly everyone implementing paths by hand.~1 means / and ~0 means ~. A key named a/b is addressed as /a~1b. Any path you build from user input must be escaped or it will resolve to the wrong node.move or remove shifts array indices for everything after it. Do not sort or dedupe a patch batch — you will change the outcome.test compares against document value, not string. {"op":"test","path":"/count","value":5} passes; {"op":"test","path":"/count","value":"5"} fails. Type mismatches between PHP ints and strings are the most common debug session with this library.json_decode($json) gives stdClass, which is usually what you want for pointer traversal. json_decode($json, true) gives arrays, which the pointer logic may handle differently for numeric keys. Test both if your models use castable JSON columns, since Laravel hydrates them as arrays.composer.json, and have a fallback plan: RFC 6902 is simple enough that a ~100-line in-house implementation is realistic if the package stalls.Patch in your own service rather than patching the vendor code. Because there is no container binding, that wrapper is also where you would swap the library out later without touching call sites.How can I help you explore Laravel packages today?