NAME

File::Raw::JSON - JSON / JSONL plugin for File::Raw

VERSION

Version 0.01

SYNOPSIS

Loading the module registers two plugins (json, jsonl) with File::Raw at BOOT time. Use them via File::Raw's standard plugin tail:

use File::Raw qw(slurp spew each_line);
use File::Raw::JSON;     # registers json + jsonl plugins

# Single JSON document
my $config = file_slurp("config.json", plugin => 'json');

# Pretty-printed write
file_spew("out.json", $payload, plugin => 'json',
          pretty => 1, sort_keys => 1);

# JSON Lines / NDJSON streaming (line-by-line, RSS bounded)
file_each_line("events.jsonl",
    sub { my $event = $_[0]; ... },
    plugin => 'jsonl');

# Whole-file JSONL into AoV
my $events = file_slurp("events.jsonl", plugin => 'jsonl');

For in-memory bytes <-> structure work (no path, no syscalls), import the direct codec:

use File::Raw::JSON qw(file_json_decode file_json_encode);

my $val   = file_json_decode($json_bytes);
my $bytes = file_json_encode($val, pretty => 1, sort_keys => 1);

# JSONL via the same entry points
my $rows  = file_json_decode($jsonl_bytes, mode => 'lines');
my $out   = file_json_encode(\@rows,       mode => 'lines');

DESCRIPTION

Fast JSON I/O integrated with the File::Raw plugin pipeline. One syscall path through File::Raw, then a direct call into yyjson (vendored, MIT).

Two plugins are registered:

json

One JSON value per file. slurp returns the parsed structure (hashref / arrayref / scalar / undef); spew serialises any Perl value back to JSON bytes. each_line with this plugin croaks with a "use 'jsonl'" message - single documents don't decompose into records.

jsonl

JSONL / NDJSON / concatenated JSON values. slurp returns an AoV; spew takes an AoV and writes one record per line; each_line streams records via callback (memory-bounded).

JSONL parsing uses brace-balancing rather than newline-splitting, mirroring JSON::Lines's $LINES regex. This means:

  • Pretty-printed JSONL works (records can span multiple lines).

  • Multiple records on one line work ({"a":1}{"b":2} = 2 records).

  • Braces inside string fields don't break parsing.

  • Chunked input is buffered and re-assembled across reads.

DIRECT CODEC

For callers who already have JSON bytes in scalar form (or who want JSON bytes back without round-tripping through a temp file), two importable XSUBs:

file_json_decode($bytes, ?key => value, ...)

Parses $bytes and returns the decoded Perl value. Default mode is document (one value per buffer); pass mode => 'lines' to parse a JSONL/NDJSON buffer and get back an arrayref of values.

my $val   = file_json_decode($json_bytes);
my $rows  = file_json_decode($jsonl_bytes, mode => 'lines');
my $cfg   = file_json_decode($cfg_bytes,   ordered => 1, relaxed => 1);

Passing undef returns undef. Malformed input croaks with the yyjson error message and byte offset.

file_json_encode($value, ?key => value, ...)

Serialises $value and returns JSON bytes. Default mode is document; pass mode => 'lines' with an arrayref payload to get back a JSONL buffer (one record per line, trailing \n).

my $bytes = file_json_encode($val);
my $jsonl = file_json_encode(\@rows, mode => 'lines');
my $diff  = file_json_encode($val,   pretty => 1, sort_keys => 1);

The trailing key/value list accepts the same options as the plugin path (see "OPTIONS" below). Odd-count tails croak; unknown keys croak.

These are XSUBs in the File::Raw::JSON package; importable on request via use File::Raw::JSON qw(file_json_decode file_json_encode) or use File::Raw::JSON qw(:codec). They share the same yyjson codec body as the plugin path, so output is byte-identical to a File::Raw::spew(... plugin => 'json', ...) for the same input.

OPTIONS

The standard plugin tail accepts these keys.

mode

document (default for the json plugin) or lines (default for jsonl). Override the plugin's default per call.

pretty

Encode: pretty-print with newlines and indent. Default false.

indent

Encode: spaces per indent level when pretty is on. Must be 2 or 4 (yyjson constraint). Arbitrary indent strings planned for v0.02.

sort_keys

Encode: emit object keys in sorted order. Off by default for speed; on for diff-friendly output.

canonical

Encode: shorthand for sort_keys => 1 + minimal whitespace.

utf8

Treat bytes as UTF-8. Default true.

relaxed

Decode: tolerate // and /* */ comments and trailing commas.

allow_nonref

Decode: accept top-level scalars (42, "hi", true). Default true.

allow_nan_inf

Round-trip NaN, Infinity, -Infinity. Non-standard JSON; default false.

ordered

Decode JSON objects as Tie::OrderedHash-tied hashes so insertion order is preserved on the Perl side. yyjson already preserves source order on parse; the regular Perl HV randomises iteration since 5.18, which is what this option works around.

my $config = file_slurp("config.json", plugin => 'json', ordered => 1);
# keys(%$config) returns in document order

my $events = file_slurp("trace.jsonl", plugin => 'jsonl', ordered => 1);
# each $events->[$i] preserves its original key order

Decode-only flag. The encoder detects the tied storage automatically and emits keys in insertion order, so a parsed-then-re-encoded ordered structure round-trips byte-for-byte without any extra flag.

Tie::OrderedHash is a hard dependency (PREREQ_PM); it ships its own public C ABI which the encoder/decoder use to bypass Perl method dispatch entirely. Earlier versions used Tie::IxHash via call_method("STORE")/call_method("FETCH") per key; the C ABI swap closes most of the per-key dispatch cost.

Cost: ordered mode is slower than the default HV path because maintaining insertion order requires bookkeeping the encoder/decoder otherwise wouldn't do. Indicative numbers on a 10k-record JSONL fixture (10 fields each):

decode default  vs  decode ordered:   ~4x slower
encode default  vs  encode ordered:   ~1.25x slower (C ABI iterator)
round-trip      vs  round-trip:        ~3x slower

The encode side is essentially free; the decode side pays the AV/HV bookkeeping that maintains the insertion-ordered key list. Use the option only when order actually matters; the default is the fast path.

boolean_class

Class to bless decoded JSON true/false into. Default File::Raw::JSON::Boolean. The encoder also recognises JSON::PP::Boolean, Types::Serialiser::Boolean, Cpanel::JSON::XS::Boolean, JSON::XS::Boolean, and the boolean module by name string.

max_depth

Decode: croak on nesting deeper than this. Default 512.

eol

JSONL only: line terminator on encode. 1-3 bytes, default \n.

VALUE MAPPING

JSON                Perl                    Encoded back as
----                ----                    ---------------
null                undef                   null
true / false        blessed sentinel        matching literal
integer (IV)        IV                      integer
integer (UV)        UV                      integer
float               NV                      float (shortest round-trip)
string              utf8-decoded SV         JSON string
array               AV ref                  [ ... ]
object              HV ref                  { ... }

STREAMING

each_line($path, $cb, plugin => 'jsonl') opens the file via File::Raw's STREAM dispatch, reads in 64 KiB chunks, accumulates bytes in a per-call buffer, and uses a brace-balancing state machine to slice off complete top-level values. Each value is parsed and passed to your callback. RSS is bounded by the read buffer + the largest single JSON value in the stream.

To abort mid-stream, die from the callback. The exception propagates and the buffer is freed.

SEE ALSO

File::Raw, JSON::Lines, https://jsonlines.org/, https://github.com/ibireme/yyjson.

AUTHOR

LNATION, <thisusedtobeanemail at gmail.com>

LICENSE AND COPYRIGHT

This software is Copyright (c) 2026 by LNATION.

This is free software, licensed under the Artistic License 2.0 (GPL Compatible).

The bundled yyjson library is Copyright (c) 2020 YaoYuan, MIT licensed - see LICENSE.yyjson.