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.

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.

max_depth($n)

Set maximum nesting depth (default 512).

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.

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
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.

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.

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
$a ne $b        # deep inequality

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.)

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

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.

LIMITATIONS

  • canonical mode is accepted but not yet implemented (yyjson has no sorted-key writer).

  • NaN and Infinity values cannot be encoded (croaks).

  • Decoding or enumerating extremely deeply nested JSON (thousands of levels) recurses on the C stack and can crash on pathological input; bound the size of untrusted JSON before decoding. Encoding is bounded by max_depth.

  • 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: 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.