Releases: boundwize/jsonrecast
Released: JsonRecast 0.0.1
JsonRecast 0.0.1 — Initial Release
A PHP JSON parser that turns JSON into an editable AST, supports visitor-based traversal, and prints changes back while preserving the original formatting.
Inspired by PHP-Parser, built for tools that need to modify JSON files safely.
JsonRecast is optimized for safely changing files. It keeps the original structure and formatting where possible, so automated tools can modify JSON without creating noisy diffs.
The AST stays clean. Change metadata lives in the traversal result.
Installation
composer require boundwize/jsonrecastFeatures
- Parse JSON into an AST
- Traverse and modify nodes with
NodeJsonTraverser - Create visitors with
NodeJsonVisitor - Access runtime traversal context with
NodeJsonPath - Replace, add, and remove JSON data
- Preserve original formatting when printing modified JSON
- Keep number representations like
1,1.0, and1e0 - Supports recursive objects and arrays
- Tracks changes outside the AST
- Dump the AST for tooling and debugging
- Designed for tooling, config updates, and automated refactoring
Quick Start
Start by parsing JSON into a document node:
use Boundwize\JsonRecast\JsonRecast;
$jsonContent = '{"name": "jsonrecast", "private": true}';
$document = JsonRecast::parse($jsonContent);The result is an AST. Dump it when you want to understand the node shape your visitors will receive:
echo JsonRecast::dumpAst($document);JsonDocument
value: ObjectNode
items:
[0]: ObjectItemNode
key: StringNode(value: "name")
value: StringNode(value: "jsonrecast")
[1]: ObjectItemNode
key: StringNode(value: "private")
value: BooleanNode(value: true)
The structure follows the JSON shape:
JsonDocumentwraps the root JSON valueObjectNodeandArrayNodecontain item nodesObjectItemNodecontains a string key and a value node- Scalar values use
StringNode,NumberNode,BooleanNode, andNullNode
Arrays use the same pattern with ArrayItemNode:
$document = JsonRecast::parse('["json", 1, null]');
echo JsonRecast::dumpAst($document);JsonDocument
value: ArrayNode
items:
[0]: ArrayItemNode
value: StringNode(value: "json")
[1]: ArrayItemNode
value: NumberNode(rawValue: "1")
[2]: ArrayItemNode
value: NullNode
After traversal, the dumper also accepts JsonRecastResult:
$result = JsonRecast::traverse($document, $visitor);
echo JsonRecast::dumpAst($result);Pass a named option when you need parser metadata:
echo JsonRecast::dumpAst($document, includeAttributes: true);You can also use the utility directly:
use Boundwize\JsonRecast\AstDumper;
echo (new AstDumper(includeAttributes: true))->dump($document);Editing and Printing
Given this JSON:
{
"name": "acme/demo",
"autoload": {
"psr-4": {
"App\\": "app/"
}
},
"autoload-dev": {
"classmap": [
"tests/Fixtures/App"
]
},
"minimum-stability": "dev"
}You can edit name, add a PSR-4 namespace, and delete stale object or array data in one traversal:
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;
$document = JsonRecast::parse($json);
$result = JsonRecast::traverse($document, new class extends NodeJsonVisitorAbstract {
public function enterNode(NodeJson $node, NodeJsonPath $path): null|NodeJson|int
{
if (
$node instanceof ObjectItemNode
&& $path->isRoot()
) {
if ($node->key->value === 'name') {
$node->value = new StringNode('boundwize/jsonrecast');
return $node;
}
if ($node->key->value === 'minimum-stability') {
return NodeJsonVisitor::REMOVE_NODE;
}
}
if ($node instanceof ObjectNode && $path->matches(['autoload', 'psr-4'])) {
$node->set('Boundwize\\JsonRecast\\', new StringNode('src/'));
return $node;
}
if ($node instanceof ArrayNode && $path->matches(['autoload-dev', 'classmap'])) {
$removed = false;
foreach ($node->items as $index => $item) {
if (! $item->value instanceof StringNode || $item->value->value !== 'tests/Fixtures/App') {
continue;
}
$node->removeAt($index);
$removed = true;
}
return $removed ? $node : null;
}
return null;
}
});
echo JsonRecast::print($result);The printed JSON keeps the surrounding formatting and only rewrites the changed pieces:
{
"name": "boundwize/jsonrecast",
"autoload": {
"psr-4": {
"App\\": "app/",
"Boundwize\\JsonRecast\\": "src/"
}
},
"autoload-dev": {
"classmap": [
]
}
}Use leaveNode() when a parent decision depends on child nodes that may already have changed. For example, after the classmap array item is removed, you can remove the now-empty root autoload-dev item:
// ...
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 null;
}
return NodeJsonVisitor::REMOVE_NODE;
}
// ...With that hook added, the printed JSON becomes:
{
"name": "boundwize/jsonrecast",
"autoload": {
"psr-4": {
"App\\": "app/",
"Boundwize\\JsonRecast\\": "src/"
}
}
}Notes
This is an early 0.0.1 release. The API should be considered unstable — breaking changes may occur in minor versions before 1.0.0.
Feedback, issues, and contributions are very welcome at https://github.com/boundwize/jsonrecast