NAME
JSON::YY - Fast JSON encoder/decoder with document manipulation API, backed by yyjson
SYNOPSIS
# functional API (fastest for simple encode/decode)
use JSON::YY qw(encode_json decode_json);
my $json = encode_json { foo => 1, bar => [1, 2, 3] };
my $data = decode_json '{"foo":1}';
# OO API (configurable)
my $coder = JSON::YY->new(utf8 => 1, pretty => 1);
my $json = $coder->encode($data);
my $data = $coder->decode($json);
# zero-copy readonly decode (fastest for read-only access)
use JSON::YY qw(decode_json_ro);
my $data = decode_json_ro $json; # readonly, zero-copy strings
# Doc API (manipulate JSON without full Perl materialization)
use JSON::YY ':doc';
my $doc = jdoc '{"users":[{"name":"Alice","age":30}]}';
jset $doc, "/users/0/age", 31;
my $name = jgetp $doc, "/users/0/name"; # "Alice"
print jencode $doc, ""; # serialize
DESCRIPTION
JSON::YY is a JSON module backed by yyjson 0.12.0, a high-performance JSON library written in ANSI C. It provides three API layers:
- Functional/Keyword API -
encode_json/decode_jsoncompiled as custom Perl ops via XS::Parse::Keyword, eliminating function call overhead. - OO API - JSON::XS-compatible interface with chaining setters.
- Doc API - Operate directly on yyjson's mutable document tree using path-based keywords. Avoids full Perl materialization for surgical JSON edits.
FUNCTIONAL API
use JSON::YY qw(encode_json decode_json decode_json_ro);
- encode_json $perl_value
-
Encode a Perl value to a UTF-8 JSON string. Equivalent to
JSON::YY->new->utf8->encode($value)but faster (no object overhead). - decode_json $json_string
-
Decode a UTF-8 JSON string to a Perl value.
- decode_json_ro $json_string
-
Decode to a deeply readonly structure with zero-copy strings. String SVs point directly into yyjson's parsed buffer. Faster than
decode_jsonfor medium/large documents. Modification attempts croak.
When imported via qw(), these compile to custom ops via XS::Parse::Keyword, bypassing normal function dispatch. Keywords are lexically scoped. The -flag import style installs pre-configured closures instead (not compiled as keywords).
OO API
my $coder = JSON::YY->new(utf8 => 1, pretty => 1);
my $coder = JSON::YY->new->utf8->pretty; # chaining style
- new(%options)
-
Create a new encoder/decoder. Options:
utf8,pretty,canonical,allow_nonref,allow_unknown,allow_blessed,convert_blessed,max_depth. (canonicalis accepted for JSON::XS compatibility but is currently a no-op; see "LIMITATIONS".)By default
allow_nonrefis on and every other flag is off, so a fresh coder produces character-mode output; passutf8 => 1for UTF-8 byte strings (as theencode_jsonfunction always does). - encode($perl_value)
-
Encode to JSON string. With
utf8enabled the result is a UTF-8 byte string; otherwise it is a character string.Strings without Perl's UTF8 flag are treated as Latin-1 and re-encoded to UTF-8 on output, as JSON::XS does, so
"caf\xE9"and"caf\x{E9}"produce the same JSON. - decode($json_string)
-
Decode from JSON string.
- decode_doc($json_string)
-
Decode to a
JSON::YY::Dochandle (mutable document, no Perl materialization). Can then use Doc API keywords on the result. - utf8, pretty, canonical, allow_nonref, allow_unknown, allow_blessed, convert_blessed
-
Boolean setters, return
$selffor chaining.convert_blessedcalls a blessed object'sTO_JSONmethod when the class has one; when it does not, encoding falls back to theallow_blessedbehaviour (null, or a croak if that flag is off). - max_depth($n)
-
Set maximum nesting depth (default 512), applied when encoding and when decoding. JSON nested deeper than this is rejected rather than materialised.
The default is what keeps untrusted input from exhausting the C stack; raising it raises that ceiling too. A few thousand is safe, but values in the tens of thousands let input through that recurses deeply enough to crash rather than croak, so only raise it as far as your data actually needs.
DOC API
use JSON::YY ':doc';
The Doc API operates on yyjson's internal mutable document tree, using JSON Pointer (RFC 6901) paths for addressing. All keywords compile to custom ops for maximum performance.
Character strings, not bytes. jdoc and jraw read their JSON as characters, like JSON::YY->new->decode does with utf8 off. If you hand them UTF-8 bytes straight off a socket or file, non-ASCII text is double-encoded. Decode first, or use the OO entry point for bytes:
my $doc = jdoc $body; # WRONG for raw bytes
my $doc = jdoc Encode::decode_utf8($body); # right
my $doc = JSON::YY->new(utf8=>1)->decode_doc($body); # right, no copy
jread reads a file as bytes and needs no such care. The keywords never modify the scalars passed to them.
Unless documented otherwise, path keywords croak when the path is missing or the value has the wrong type for the operation. The exceptions return a soft value instead: jgetp, jtype, jdel, and jfind return undef; jhas and the jis_* predicates return false.
Document creation
- jdoc $json_string
-
Parse JSON into a mutable document handle (
JSON::YY::Doc). - jfrom $perl_value
-
Create a document from a Perl value (hash, array, scalar).
Value constructors
Create typed JSON values for use with jset:
- jstr $value - JSON string (ensures string type, e.g.
jstr "007") - jnum $value - JSON number (croaks if
$valueis not numeric) - jbool $value - JSON true/false
- jnull - JSON null
- jarr - empty JSON array
- jobj - empty JSON object
Path operations
All path arguments use JSON Pointer syntax: /key/0/nested. Use "" for root. Use /arr/- to append to an array.
- jget $doc, $path
-
Get a subtree reference (returns a Doc that shares the parent's tree). Croaks if path not found. Use
jhasto check first, orjgetpfor undef-on-missing behavior. - jgetp $doc, $path
-
Get value materialized to Perl (string, number, hashref, arrayref, etc.). Alias:
jdecode. - jset $doc, $path, $value
-
Set value at path.
$valuecan be a scalar (auto-typed), Perl ref (recursively converted), or another Doc (deep-copied). Returns$doc.Missing intermediate levels are created as objects, even when the path component is a number, so
jset $doc, "/users/0/name", "Bob"on an empty document yields{"users":{"0":{"name":"Bob"}}}, not an array. Create the array first (jset $doc, "/users", []) when you want one; appending to an existing array with/-works as expected.A blessed object is converted with its
TO_JSONmethod if it has one, and otherwise becomesnull(the Doc API always behaves as ifconvert_blessedandallow_blessedwere enabled). The same applies tojfrom. - jdel $doc, $path
-
Delete value at path. Returns the removed subtree as an independent Doc, or
undefif path not found. Croaks on an empty path (the root cannot be deleted). - jhas $doc, $path
-
Check if path exists. Returns boolean.
- jclone $doc, $path
-
Deep copy subtree into a new independent document.
Serialization
- jencode $doc, $path
-
Serialize document or subtree to compact JSON bytes.
- jpp $doc, $path
-
Serialize to pretty-printed JSON (indented with 4 spaces).
- jraw $doc, $path, $json_fragment
-
Insert a raw JSON string at path without Perl roundtrip. The fragment is parsed by yyjson and inserted directly into the document tree.
Inspection
- jtype $doc, $path
-
Returns type string:
"object","array","string","number","boolean","null". - jlen $doc, $path
-
Array length, object key count, or string byte length.
- jkeys $doc, $path
-
Object keys as a list of strings.
- jvals $doc, $path
-
Object values as a list of Doc handles.
Iteration
Pull-style iterators for arrays and objects:
my $it = jiter $doc, "/users";
while (defined(my $elem = jnext $it)) {
my $name = jgetp $elem, "/name";
my $key = jkey $it; # for objects: current key
}
- jiter $doc, $path - create iterator
- jnext $iter - advance, returns Doc or undef
- jkey $iter - current key (objects only)
File I/O
- jread $filename
-
Read a JSON file and return a Doc handle.
- jwrite $doc, $filename
-
Write a Doc to a file (pretty-printed).
Path enumeration
- jpaths $doc, $path
-
Enumerate all leaf paths under the given path. Returns a list of JSON Pointer strings. Keys containing
~or/are escaped per RFC 6901. Empty objects and arrays contain no leaves and so contribute no paths.
Search
- jfind $doc, $array_path, $key_path, $match_value
-
Find the first element in an array where the value at
$key_pathequals$match_value. Returns the matching element as a Doc, orundefif no element matches (also if$array_pathis missing or does not point to an array).my $bob = jfind $doc, "/users", "/name", "Bob";Integer fields are compared as 64-bit integers and real fields as doubles (so values above 2^53 do not collide). To match a JSON
true,false, ornullfield, pass the corresponding string"true","false", or"null"as$match_value.
Patching
- jpatch $doc, $patch_doc
-
Apply RFC 6902 JSON Patch.
$patch_docmust be a Doc containing a patch array. Modifies$docin-place.$docmust be an owned document; croaks on a borrowed subtree (fromjget) --jcloneit first. - jmerge $doc, $patch_doc
-
Apply RFC 7386 JSON Merge Patch. Modifies
$docin-place. As withjpatch,$docmust be an owned document, not a borrowed subtree.
Comparison
Type predicates
All return boolean. Return false for missing paths.
- jis_obj $doc, $path
- jis_arr $doc, $path
- jis_str $doc, $path
- jis_num $doc, $path
- jis_int $doc, $path
- jis_real $doc, $path
- jis_bool $doc, $path
- jis_null $doc, $path
Overloading
JSON::YY::Doc objects support:
"$doc" # stringify to JSON
if ($doc) # always true
$a eq $b # deep equality; vs a plain string, compares the JSON
$a ne $b # deep inequality
$a == $b # identity -- true only for the same handle
IMPORT FLAGS
use JSON::YY -utf8, -pretty;
Imports encode_json/decode_json with the specified flags pre-configured. (decode_json_ro is only available via the qw() import, not the flag form.)
The two import styles do not combine: the flag form installs closures, so it turns off the keyword compilation for those two names in that scope. Import one way or the other.
JSON POINTER (RFC 6901)
Paths use JSON Pointer syntax:
"" root value
/key object key
/0 array index 0
/a/b/0/c nested path
/arr/- append to array (jset/jraw only)
/k~0ey key containing ~ (escaped as ~0)
/k~1ey key containing / (escaped as ~1)
EXAMPLES
# surgical edit of large document
use JSON::YY ':doc';
my $doc = jdoc $large_json;
jset $doc, "/config/timeout", 30;
my $json = jencode $doc, "";
# extract fields without full decode
my $doc = jdoc $api_response;
my $status = jgetp $doc, "/status";
my $count = jlen $doc, "/data/items";
# type-safe value insertion
jset $doc, "/active", jbool 1; # true, not 1
jset $doc, "/id", jstr "007"; # "007", not 7
# iterate without materializing
my $it = jiter $doc, "/users";
while (defined(my $u = jnext $it)) {
say jgetp $u, "/name" if jis_str $u, "/name";
}
# apply RFC 6902 patch
my $patch = jdoc '[{"op":"replace","path":"/v","value":2}]';
jpatch $doc, $patch;
# apply RFC 7386 merge patch
jmerge $doc, jdoc '{"debug":null,"version":"2.0"}';
# OO decode directly to Doc
my $coder = JSON::YY->new(utf8 => 1);
my $doc = $coder->decode_doc($json);
# insert raw JSON without Perl roundtrip
jraw $doc, "/blob", '[1,2,{"nested":true}]';
# deep compare
say "equal" if jeq $doc_a, $doc_b;
say "equal" if $doc_a eq $doc_b; # overloaded
PERFORMANCE
Indicative figures from the bundled bench/bench.pl. Throughput depends heavily on payload shape, perl build and hardware, and the two libraries trade places across those axes -- run the benchmark on your own data rather than relying on the numbers below.
Encode (ops/sec, higher is better)
JSON::XS JSON::YY delta
small (38B) 6.4M 6.7M +4%
medium (11KB) 26.8K 27.3K +2%
large (806KB) 153 234 +53%
Decode (ops/sec, higher is better)
JSON::XS JSON::YY delta
small (38B) 4.2M 3.5M -17%
medium (11KB) 16.9K 14.1K -16%
large (806KB) 249 267 +8%
Encode is consistently faster, especially on large payloads where yyjson's optimized serializer dominates. Decode is slightly slower on small/medium payloads due to Perl SV allocation overhead.
Doc API vs decode-modify-encode cycle
Perl Doc speedup
read one value 3.0M/s 3.1M/s ~equal
modify + serialize 1.6M/s 2.2M/s +42%
read from large doc 14.6K/s 73.7K/s +405%
modify large + encode 7.4K/s 47.3K/s +536%
clone subtree 15.0K/s 75.2K/s +400%
type/length check 14.4K/s 74.6K/s +418%
The Doc API avoids full Perl materialization, providing 4-5x speedup for surgical operations on medium/large documents.
THREADS
Encoding and decoding are stateless, so encode_json, decode_json and decode_json_ro can be used freely from any thread.
Coder objects (JSON::YY->new) are copied into a new thread with their settings intact and remain usable on both sides.
JSON::YY::Doc handles and iterators are tied to the interpreter that created them: they may be alive when a thread starts, but the copy the child receives is inert and using it croaks with cannot be shared between threads. The original stays fully usable. Pass JSON text (or the result of jencode) between threads instead of a Doc.
Structures from decode_json_ro may be shared with a thread and read from either side; the underlying parse buffer is kept alive until the last thread holding part of it goes away.
LIMITATIONS
canonicalmode is accepted but not yet implemented (yyjson has no sorted-key writer).Duplicate keys in one object are resolved differently by the two decoders:
decode_json/decode_json_rokeep the last occurrence (as JSON::XS does), while the Doc API resolves a pointer to the first.jdelon such a key removes every occurrence but returns only the first. JSON does not define duplicate-key semantics; avoid relying on either.NaN and Infinity values cannot be encoded (croaks).
Nesting is bounded in both directions by
max_depth(default 512): input nested deeper than that is rejected withmaximum nesting depth exceededrather than being materialised, and encoding croaks the same way. A JSON Pointer passed tojset/jrawis bounded too, since each component creates a nested parent. The functional and Doc APIs use the default; only the OO coder can change it -- see "max_depth($n)" before raising it far.A document can still be driven past the limit by repeated mutation, since each individual path is short (
jset $cur, "/k", {}; $cur = jget $cur, "/k"in a loop). Serialising such a document is safe (jencode,jppand stringification are iterative), andjgetp/jpathscroak rather than recurse.jclone,jeq,jdel,jwriteandjpatchhowever recurse inside yyjson and will exhaust the C stack somewhere around 100_000 levels. Reaching that takes a deliberate loop -- neither parsed JSON nor a JSON Pointer can build a document that deep -- but if you construct documents by unbounded repeated nesting, serialise rather than clone or compare them.JSON
true/falsedecode to the Perl scalars1/0(correct in boolean context), not to overloaded boolean objects. To encode a JSON boolean, pass a scalar ref (\1for true,\0for false) or usejboolin the Doc API. Consequentlyencode_json(decode_json('[true,false]'))yields[1,0], not[true,false].
COOKBOOK
Read config, modify, write back
use JSON::YY ':doc';
my $config = jread "config.json";
jset $config, "/database/host", "newhost";
jwrite $config, "config.json";
Extract fields from large API response
my $doc = jdoc $response_body;
my $status = jgetp $doc, "/status";
my $count = jlen $doc, "/data/items";
my $first = jgetp $doc, "/data/items/0/name";
Find user by name in array
my $user = jfind $doc, "/users", "/name", "Alice";
say jgetp $user, "/email" if defined $user;
Build document from scratch
my $doc = jfrom {};
jset $doc, "/name", "My App";
jset $doc, "/version", jnum 1;
jset $doc, "/features", jarr;
jset $doc, "/features/-", "auth";
jset $doc, "/features/-", "logging";
jset $doc, "/debug", jbool 0;
jwrite $doc, "output.json";
Apply incremental updates (merge patch)
my $doc = jread "state.json";
jmerge $doc, jdoc $incoming_patch_json;
jwrite $doc, "state.json";
Debug: show all paths
my @paths = jpaths $doc, "";
say "$_ = ", jencode $doc, $_ for @paths;
Type-safe assertions
die "expected array" unless jis_arr $doc, "/items";
die "expected string" unless jis_str $doc, "/name";
Compare two documents
die "configs differ" if $prod ne $staging; # overloaded
# or explicitly:
die "differ" unless jeq $prod, $staging;
CHEATSHEET
# --- Import ---
use JSON::YY qw(encode_json decode_json); # functional
use JSON::YY ':doc'; # Doc API keywords
# --- Encode/Decode ---
encode_json $data decode_json $json
$coder->encode($data) $coder->decode($json)
decode_json_ro $json # zero-copy readonly
# --- Doc lifecycle ---
jdoc $json # parse JSON string -> Doc
jfrom $perl_data # Perl data -> Doc
jread $file # read JSON file -> Doc
jwrite $doc, $file # Doc -> write JSON file
jencode $doc, $path # Doc -> JSON string
jpp $doc, $path # Doc -> pretty JSON string
jgetp $doc, $path # Doc -> Perl value
$coder->decode_doc($json) # OO: JSON -> Doc
# --- Read ---
jget $doc, $path # -> Doc subtree ref (shared)
jgetp $doc, $path # -> Perl value (materialized)
jdecode $doc, $path # alias for jgetp
jhas $doc, $path # -> bool
jfind $doc, $arr, $k, $v # -> Doc (first match) or undef
# --- Write ---
jset $doc, $path, $val # set (scalar/ref/Doc)
jdel $doc, $path # delete -> Doc (removed)
jraw $doc, $path, $json # insert raw JSON fragment
# --- Copy ---
jclone $doc, $path # deep copy -> independent Doc
# --- Inspect ---
jtype $doc, $path # "object"|"array"|"string"|...
jlen $doc, $path # array/object/string length
jkeys $doc, $path # object keys (list)
jvals $doc, $path # object values (list of Doc)
jpaths $doc, $path # all leaf paths (list)
# --- Type predicates ---
jis_obj jis_arr jis_str jis_num jis_int jis_real jis_bool jis_null
# --- Value constructors ---
jstr $v jnum $v jbool $v jnull jarr jobj
# --- Iterate ---
my $it = jiter $doc, $path;
while (defined(my $v = jnext $it)) { jkey $it; ... }
# --- Patch ---
jpatch $doc, $patch # RFC 6902
jmerge $doc, $patch # RFC 7386
# --- Compare ---
jeq $a, $b # deep equality
$a eq $b # overloaded
"$doc" # overloaded stringify
# --- Path syntax (JSON Pointer RFC 6901) ---
"" root /key object key
/0 array[0] /arr/- append to array
/k~0ey key with ~ /k~1ey key with /
SEE ALSO
JSON::XS, Cpanel::JSON::XS, JSON::PP
yyjson: https://github.com/ibireme/yyjson
AUTHOR
vividsnow
LICENSE
This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
yyjson is included under the MIT License.