Editable JSON AST with visitor traversal and formatting-preserving printing.
JsonRecast parses JSON into an editable AST, lets you transform it with path-aware visitors, and prints the result while keeping the original formatting where possible. Its traversal model is inspired by nikic/PHP-Parser, applied to JSON documents for tools that update files without creating noisy diffs.
composer require boundwize/jsonrecastThis example updates a composer.json-style document in one traversal:
- Parse the JSON into a
JsonDocument. - Traverse the document with a visitor and return changed nodes.
- Replace the root
namevalue. - Remove the root
minimum-stabilityentry. - Add a PSR-4 namespace under
autoload.psr-4. - Remove a stale value from
autoload-dev.classmap. - In
leaveNode(), remove the now-emptyautoload-devparent. - Print the result while preserving the original formatting style.
use Boundwize\JsonRecast\JsonRecast;
use Boundwize\JsonRecast\Node\ArrayNode;
use Boundwize\JsonRecast\Node\NodeJson;
use Boundwize\JsonRecast\Node\ObjectItemNode;
use Boundwize\JsonRecast\Node\ObjectNode;
use Boundwize\JsonRecast\Node\StringNode;
use Boundwize\JsonRecast\NodePath\NodeJsonPath;
use Boundwize\JsonRecast\NodeVisitor\NodeJsonVisitor;
use Boundwize\JsonRecast\NodeVisitor\NodeJsonVisitorAbstract;
$json = <<<'JSON'
{
"name": "acme/demo",
"autoload": {
"psr-4": {
"App\\": "app/"
}
},
"autoload-dev": {
"classmap": [
"tests/Fixtures/App"
]
},
"minimum-stability": "dev"
}
JSON;
// 1. Parse the source JSON into an editable document node.
$document = JsonRecast::parse($json);
// 2. Traverse the document and return changed nodes so JsonRecast can track edits.
$result = JsonRecast::traverse($document, new class extends NodeJsonVisitorAbstract {
public function enterNode(NodeJson $node, NodeJsonPath $path): null|NodeJson|int
{
if ($node instanceof ObjectItemNode && $path->isRoot()) {
// 3. Replace the root "name" value.
if ($node->key->value === 'name') {
$node->value = new StringNode('boundwize/jsonrecast');
return $node;
}
// 4. Remove an unwanted root object item.
if ($node->key->value === 'minimum-stability') {
return NodeJsonVisitor::REMOVE_NODE;
}
}
// 5. Add a new namespace under "autoload.psr-4".
if ($node instanceof ObjectNode && $path->matches(['autoload', 'psr-4'])) {
$node->set('Boundwize\\JsonRecast\\', new StringNode('src/'));
return $node;
}
// 6. Remove a stale classmap entry.
if ($node instanceof ArrayNode && $path->matches(['autoload-dev', 'classmap'])) {
foreach ($node->items as $index => $item) {
if ($item->value instanceof StringNode && $item->value->value === 'tests/Fixtures/App') {
$node->removeAt($index);
return $node;
}
}
}
return null;
}
// 7. After child edits are done, remove the empty "autoload-dev" parent.
public function leaveNode(NodeJson $node, NodeJsonPath $path): ?int
{
if (
! $node instanceof ObjectItemNode
|| ! $path->isRoot()
|| $node->key->value !== 'autoload-dev'
|| ! $node->value instanceof ObjectNode
) {
return null;
}
$classmapItem = $node->value->get('classmap');
if (
$classmapItem instanceof ObjectItemNode
&& $classmapItem->value instanceof ArrayNode
&& $classmapItem->value->items === []
) {
return NodeJsonVisitor::REMOVE_NODE;
}
return null;
}
});
// 8. Print with the original formatting preserved where possible.
echo JsonRecast::print($result);That will output:
{
"name": "boundwize/jsonrecast",
"autoload": {
"psr-4": {
"App\\": "app/",
"Boundwize\\JsonRecast\\": "src/"
}
}
}Read the full documentation at https://boundwize.github.io/jsonrecast/.