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

