fungio/json-builder
A Laravel/PHP package for assembling JSON programmatically. It provides a fluent, chainable API to build nested arrays and objects cleanly, so you can generate structured JSON for APIs, configs, or dynamic payloads without hand-writing arrays. Keeps complex JSON construction readable and maintainabl...
composer require fungio/json-builder
The package is a single, tiny value object. Everything you need is in src/JsonBuilder.php and its test suite — read those two files before anything else; the whole library is a few hundred lines and the test file is effectively the documentation. Ignore the README's marketing of "escaping control" until you've seen the tests, because the escaping behaviour is best understood from the assertions.
The mental model is: a builder that holds an ordered key → value map, where each value is either "escape this for me" (passed through json_encode) or "insert this verbatim" (assumed already-valid JSON). Serialization happens once at the end via getJson().
First use case — a controller returning a payload where one field is already serialized JSON (a cached fragment, a json DB column, a sub-resource another code path produced):
use Ivory\JsonBuilder\JsonBuilder;
$builder = new JsonBuilder();
$builder
->setValue('id', 42) // escaped by the builder
->setValue('name', 'Ada "Countess" Lovelace')
->setRawValue('meta', $cachedJsonFragment) // inserted as-is
;
return new Response($builder->getJson(), 200, ['Content-Type' => 'application/json']);
Verify the exact setter names against src/ before pasting into a codebase — the surface is setValue / setRawValue-shaped, and the raw variant is the reason to install this package at all.
Wrap it, don't sprinkle it. Instantiate the builder in one presentation-layer class (a JsonPresenter, ApiResource, or a small JsonResponseFactory) so the "raw vs escaped" decision lives in exactly one place:
final class JsonResponseFactory
{
public function create(array $data): Response
{
$builder = new JsonBuilder();
foreach ($data as $key => $value) {
if ($value instanceof RawJson) { // your own marker value object
$builder->setRawValue($key, (string) $value);
continue;
}
$builder->setValue($key, $value);
}
return new Response($builder->getJson(), 200, ['Content-Type' => 'application/json']);
}
}
The RawJson marker is the single most valuable thing you can add on top of this package — it makes "this string is trusted, pre-serialized JSON" a type rather than a convention, and it stops the setRawValue call sites from multiplying.
Composite builders for nested resources. Build child fragments independently and stitch them at the boundary. This is the pattern the library is genuinely good at, because a plain json_encode($array) cannot embed an already-rendered child without a round-trip through json_decode:
$user = (new JsonBuilder())
->setValue('id', $user->getId())
->setValue('email', $user->getEmail());
$post = (new JsonBuilder())
->setValue('id', $post->getId())
->setRawValue('author', $user->getJson()) // no decode/re-encode cycle
->setValue('title', $post->getTitle());
Caching pre-serialized fragments. Because getJson() returns a string, a built fragment is a natural cache value. Store the string, fetch it later, and re-embed with setRawValue — you skip json_decode → mutate → json_encode entirely on cache hits. This is the strongest day-to-day argument for the package: it turns JSON assembly into string concatenation with no intermediate PHP array.
Ordered output. PHP arrays preserve insertion order and the builder serializes in insertion order, so field order is deterministic. If a consumer (or a snapshot test) depends on key order, set fields in a fixed sequence at the factory level rather than relying on array iteration order from a DTO.
Testing. Always assert on both sides of the boundary: json_decode($builder->getJson(), true) must be an array (proves the output is valid JSON), and assert the raw string for the cases that matter (proves raw insertion didn't get re-escaped). A test that only decodes will happily pass on output where someone double-encoded a raw fragment.
Integration. In Symfony/Laravel this slots in as a plain service — it has no framework coupling. Register the JsonResponseFactory as a singleton, inject it into controllers, and keep the builder itself as a short-lived per-request object (it is stateful and mutable; do not share it).
setRawValue payload is legal JSON. A null fragment, an empty string, or a PHP array passed to the raw setter will silently produce malformed output that only fails at the consumer. Validate at the boundary: json_decode($fragment) returning non-null is a cheap gate, or wrap raw values in the RawJson marker and only construct it from a getJson() call."{\"a\":1}"). The failure is invisible in your own tests unless you assert on the raw string.json_encode as numbers and booleans; raw fragments bypass that. A raw "1" is a string, a raw 1 is a number. If a client does strict type checks, this will bite you./ and non-ASCII to \uXXXX applies. If your API contract expects literal UTF-8, check whether the builder lets you pass JSON_UNESCAPED_UNICODE / JSON_UNESCAPED_SLASHES; if it doesn't expose flags, you're stuck with the default and should post-process or reconsider.json_encode returns false on malformed UTF-8 in some PHP versions and emits null in others. The builder almost certainly does not check for false before concatenating, so a single bad byte turns the whole response into garbage. Validate user-supplied strings before they hit the builder.{} or []. An empty builder serializes as one of the two, and consumers that switch on it will break if you change your mind later. Encode the decision in the factory, not at call sites.related graph that loops back.json_encode($array, JSON_UNESCAPED_UNICODE) is sufficient for most payloads — this package only earns its place when you genuinely need to embed pre-serialized fragments without a decode/re-encode cycle.How can I help you explore Laravel packages today?