Skip to content

Releases: boundwize/jsonrecast

Released: JsonRecast 0.0.1

28 Jun 02:48
0.0.1
e5b09a3

Choose a tag to compare

JsonRecast 0.0.1 — Initial Release

JsonRecast Logo

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.

Latest Version ci build Code Coverage PHPStan Downloads

Windows macOS Linux

Installation

composer require boundwize/jsonrecast

Features

  • 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, and 1e0
  • 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:

  • JsonDocument wraps the root JSON value
  • ObjectNode and ArrayNode contain item nodes
  • ObjectItemNode contains a string key and a value node
  • Scalar values use StringNode, NumberNode, BooleanNode, and NullNode

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