cerbero/json-parser
A PHP/Laravel package for parsing large JSON files efficiently with low memory. Use JSONPath-style queries to extract exactly the data you need, stream huge documents without loading them fully, and integrate cleanly into Laravel apps for imports, APIs, and data processing.
Install the package with Composer:
composer require cerbero/json-parser
It is framework-agnostic — no service provider or Laravel config is required. Check composer.json for the exact PHP constraint before adopting it, then start with the package README and src/JsonParser.php; the source is small and the tests are the best documentation for pointer and source behaviour.
Point it at any JSON source — a file path, a URL, an open stream resource, or a raw JSON string:
use Cerbero\JsonParser\JsonParser;
foreach (JsonParser::parse(storage_path('app/export.json')) as $key => $item) {
// One item is held in memory at a time.
}
Your first real use case will almost always be: "I have a large JSON export and I need to import it without blowing up memory_limit." Write a throwaway Artisan command, point the parser at the branch that contains the records, and insert them into the database in chunks.
Most APIs wrap records in metadata. Use a JSON Pointer to skip straight to the array you care about:
$items = JsonParser::parse($path)->pointer('/data/items');
foreach ($items as $item) {
// $item is the decoded record
}
This also avoids decoding sibling metadata you never use.
Do not call Model::create() per row. Buffer and flush:
$buffer = [];
foreach (JsonParser::parse($path)->pointer('/data/items') as $item) {
$buffer[] = [
'external_id' => $item['id'],
'name' => $item['name'],
'created_at' => now(),
'updated_at' => now(),
];
if (count($buffer) >= 500) {
DB::table('items')->upsert($buffer, ['external_id'], ['name', 'updated_at']);
$buffer = [];
}
}
if ($buffer) {
DB::table('items')->upsert($buffer, ['external_id'], ['name', 'updated_at']);
}
Use upsert when the import may be re-run. Streaming parsers have no resume cursor, so idempotency is your recovery strategy.
Long imports need feedback. Feed the parser's progress callback into your command output:
$parser = JsonParser::parse($path)->pointer('/data/items');
$parser->progress(function ($progress) {
$this->output->write(sprintf(
"\r%d / %d bytes",
$progress->bytes(),
$progress->total()
));
});
Keep a --dry-run flag that iterates but skips writes; it validates both the JSON and your field mapping cheaply.
Keep pointer strings and row mapping out of controllers and jobs. A generator-based importer makes the streaming nature explicit:
final class ItemImporter
{
/** @return \Generator<int, array> */
public function rows(string $path): \Generator
{
$parser = JsonParser::parse($path)->pointer('/data/items');
foreach ($parser as $item) {
yield ItemData::fromArray($item);
}
}
}
Callers can then chunk, count, or test the importer with a small fixture without knowing anything about the parser.
Process a file in a dedicated job, keep chunk size modest, and set job/worker timeouts explicitly:
final class ImportJsonFile implements ShouldQueue
{
public int $timeout = 900;
public int $tries = 1;
public function __construct(public string $path) {}
public function handle(): void
{
// chunked upsert loop as above
}
}
Prefer upserts over "delete then insert" so a failed or retried job is safe.
JsonParser::parse() accepts paths, URLs, resources, and strings. For authenticated or customised HTTP sources, open the stream yourself and pass the resource — that keeps headers, timeouts, and TLS options under your control:
$context = stream_context_create([
'http' => [
'header' => "Authorization: Bearer {$token}\r\n",
'timeout' => 30,
],
]);
$stream = fopen('https://api.example.com/export.json', 'rb', false, $context);
foreach (JsonParser::parse($stream)->pointer('/data/items') as $item) {
// ...
}
fclose($stream);
Fixtures do not need to be large. Test pointer selection, missing keys, and malformed JSON with small inline strings:
$parser = JsonParser::parse('{"data":{"items":[{"id":1}]}}')->pointer('/data/items');
collect($parser) or build a full array of results "for later". Yield, buffer in fixed-size chunks, and discard./data/items); escaped characters follow JSON Pointer rules (~0 for ~, ~1 for /). A wrong pointer is a common cause of "it parsed but yielded nothing".foreach. Wrap the loop in try/catch and decide whether a partially imported file should be rolled back or upserted over.allow_url_fopen and network stability. For anything mission-critical, download to a temp file, validate, then parse locally. Progress "total" is also unreliable for non-seekable remote streams.null values need explicit handling. Add validation or DTO construction at the boundary rather than deep inside the loop.% is fine; using it to decide chunk boundaries or cancellation is not. Track your own row counter.$timeout on the job, run a worker with a compatible --timeout, and monitor failures.JsonParser::parse() in one importer/service, and add a small integration test with a fixture — that makes swapping or patching the parser later a one-file change.How can I help you explore Laravel packages today?