Repository navigation
First release.
Features
JsonFuse\Plan::compileandJsonFuse\Runner::process: a one-call NDJSON validate, filter and project pipeline with typed fields (int64,float64,bool,string, nullable variants), filters, a strict record-size cap and bounded native memory.maxRecordBytesdefaults to 1,048,576 and ranges from 1 to 67,108,864.- Field specs in array form,
'name' => ['type' => ..., 'default' => ...], and optional fields: a missing key emits the declared default, a presentnullnever takes it. The default must have the declared type at compile time; afloat64default may also be an int that binary64 holds exactly. Any other key in a field spec raisesPlanExceptionunknown_option. - Nested field paths:
'name' => ['type' => ..., 'path' => ['a', 'b']]reads a value 1 to 8 object levels deep, and the output stays flat.whereentries take the same paths. Reason codesinvalid_pathandpath_not_object; two fields with the same path raiseduplicate_name, andduplicate_keycovers a key repeated in any object on a plan path. whereoperators: an entry is a scalar (['=' => value]) or an array of an optionalpathand one or more of=,>,>=,<,<=andin, all ANDed.intakes a list of 1 to 256 scalars. Range bounds are numbers, compared with record integers and reals by exact value, or strings, compared with record strings bytewise after unescaping; other record kinds, raw big numbers included, never match a range.JsonFuse\StatswithrecordsRead,recordsMatched,recordsEmitted,bytesRead,bytesWritten.JsonFuse\PlanException,RecordExceptionandStreamException, all extendingJsonFuseException. Every exception has a stable reason code (getReason()) andgetField();PlanExceptionaddsgetOption();RecordExceptionandStreamExceptionaddgetInputLine(),getRecordNumber(),getByteOffset()andgetProgress(), aStatsof the counts at the throw. Messages never contain record bytes; their text is informational, so match ongetReason(). Theline N (record R, offset O): reasonprefix is kept.- After a record error,
read_failedorno_data, the complete records buffered before the failure are written first, withoutphp_stream_flush, and then the exception is raised. If that write fails, the exception isStreamExceptionwrite_failedwith the recorded exception as its previous; an exception thrown by a wrapper or filter during that write propagates with the recorded exception at the end of itsgetPrevious()chain. - After a
Runner::processcall during which any fiber switch happened, both streams keepPHP_STREAM_FLAG_NO_FCLOSE:fclose()on them returns false with anE_WARNING, and a new run on them raisesStreamException. Each stream is still freed when its last reference is released. This avoids a use-after-free in PHP core when a fiber is suspended inside a filter or wrapper callback of one of the streams. A module loaded withdl()cannot observe switches, so there every run leaves both flags set. - A validating scan parses the records of plans whose paths all have one segment, without building a yyjson document. Records it does not accept (any syntax error, invalid UTF-8, a duplicate referenced key, an escaped top-level key, a RAW number in a referenced member, nesting deeper than 64) are parsed by yyjson, so results and messages do not change.
-DJF_DISABLE_SCANbuilds without it;php --ri jsonfusereportsvalidating scan. - Vendored yyjson 0.13.0, unmodified, with its symbols hidden.
- Platforms: PHP 8.3 to 8.6, NTS and ZTS, Linux x86-64 and aarch64, and macOS arm64. PIE metadata in
composer.jsonsetssupport-ztsand thelinuxanddarwinOS families. - The source package (
git archive) does not containtests/,fuzz/,benchmarks/or.github/. The repository carries a PHPT suite, a libFuzzer harness over the native core, a multi-thread harness run under ThreadSanitizer, a benchmark harness againstjson_decode, PECL simdjson,fastjsonand JSON Machine, and CI for every supported platform, including ASan/UBSan lanes. SECURITY.mdandRELEASING.md.
Performance
Measured on x86-64 (see benchmarks/RESULTS.md): 3.55x to 5.06x faster than a json_decode loop on 100k records and above, 2.08x to 2.63x faster than fastjson on narrow records at 100k records and above, and 1.53x to 1.71x faster than fastjson on wide records.
Known limitations
See docs/THREAT_MODEL.md. In short: no general-purpose JSON use, and output is not transactional.