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_json" compiled 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_json" for 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". ("canonical" is accepted for JSON::XS compatibility but is currently a no-op; see "LIMITATIONS".) By default "allow_nonref" is on and every other flag is off, so a fresh coder produces character-mode output; pass "utf8 => 1" for UTF-8 byte strings (as the "encode_json" function always does). encode($perl_value) Encode to JSON string. With "utf8" enabled 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::Doc" handle (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 $self for chaining. "convert_blessed" calls a blessed object's "TO_JSON" method when the class has one; when it does not, encoding falls back to the "allow_blessed" behaviour ("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 $value is 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 "jhas" to check first, or "jgetp" for 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. $value can 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_JSON" method if it has one, and otherwise becomes "null" (the Doc API always behaves as if "convert_blessed" and "allow_blessed" were enabled). The same applies to "jfrom". jdel $doc, $path Delete value at path. Returns the removed subtree as an independent Doc, or "undef" if 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_path equals $match_value. Returns the matching element as a Doc, or "undef" if no element matches (also if $array_path is 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", or "null" field, pass the corresponding string "true", "false", or "null" as $match_value. Patching jpatch $doc, $patch_doc Apply RFC 6902 JSON Patch. $patch_doc must be a Doc containing a patch array. Modifies $doc in-place. $doc must be an owned document; croaks on a borrowed subtree (from "jget") -- "jclone" it first. jmerge $doc, $patch_doc Apply RFC 7386 JSON Merge Patch. Modifies $doc in-place. As with "jpatch", $doc must be an owned document, not a borrowed subtree. Comparison jeq $doc_a, $doc_b Deep equality comparison. Returns boolean. 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 * "canonical" mode 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_ro" keep the last occurrence (as JSON::XS does), while the Doc API resolves a pointer to the first. "jdel" on 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 with "maximum nesting depth exceeded" rather than being materialised, and encoding croaks the same way. A JSON Pointer passed to "jset"/"jraw" is 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", "jpp" and stringification are iterative), and "jgetp"/"jpaths" croak rather than recurse. "jclone", "jeq", "jdel", "jwrite" and "jpatch" however 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"/"false" decode to the Perl scalars 1/0 (correct in boolean context), not to overloaded boolean objects. To encode a JSON boolean, pass a scalar ref ("\1" for true, "\0" for false) or use "jbool" in the Doc API. Consequently "encode_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: 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.