NAME

mb::JSON - JSON encode/decode for multibyte (UTF-8) strings

VERSION

Version 0.07

SYNOPSIS

use mb::JSON;

# decode: JSON text -> Perl data
my $decoded = mb::JSON::decode("{\"name\":\"\\u7530\\u4e2d\",\"age\":30}");

# parse: alias for decode()
my $parsed = mb::JSON::parse("{\"key\":\"value\"}");

# encode: Perl data -> JSON text
my $json = mb::JSON::encode({ name => 'Tanaka', age => 30 });
# -> '{"age":30,"name":"Tanaka"}'

# stringify: alias for encode()
my $text = mb::JSON::stringify({ name => 'Tanaka', age => 30 });
# -> '{"age":30,"name":"Tanaka"}'

# Boolean values
my $flags = mb::JSON::encode({
    active => mb::JSON::true,
    locked => mb::JSON::false,
});
# -> '{"active":true,"locked":false}'

# null
my $empty = mb::JSON::encode({ value => undef });
# -> '{"value":null}'

TABLE OF CONTENTS

DESCRIPTION

mb::JSON is a simple, dependency-free JSON encoder and decoder designed for Perl 5.005_03 and later. It handles UTF-8 multibyte strings correctly, making it suitable for environments where standard JSON modules requiring Perl 5.8+ are unavailable.

mb::JSON provides two pairs of symmetric functions: decode()/parse() convert JSON text to Perl data, and encode()/stringify() convert Perl data to JSON text. Within each pair, both names are aliases and produce identical output.

The decoder scans its input with pos() and the \G anchor and never modifies the buffer, so decoding time is linear in the length of the input. See "PERFORMANCE".

FUNCTIONS

decode( $json_text )

Converts a JSON text string to a Perl data structure. If the argument is omitted or undefined, $_ is used. A leading UTF-8 byte order mark is ignored.

my $data = mb::JSON::decode($json_text);

parse( $json_text )

Alias for decode(). Both names are interchangeable.

my $data = mb::JSON::parse($json_text);

encode( $data )

Converts a Perl data structure to a JSON text string. Returns a byte string encoded in UTF-8. If called with no argument at all, $_ is used; note that encode(undef) still returns null.

my $json = mb::JSON::encode($data);

stringify( $data )

Alias for encode(). Both names are interchangeable. Mirrors the JSON.stringify() function in JavaScript.

my $json = mb::JSON::stringify($data);

true

Returns the singleton mb::JSON::Boolean object representing JSON true. Numifies to 1, stringifies to "true".

false

Returns the singleton mb::JSON::Boolean object representing JSON false. Numifies to 0, stringifies to "false".

CONFIGURATION

Two package variables tune the behaviour of the module. Both are ordinary globals, so local works as expected.

$mb::JSON::MAX_DEPTH

Maximum nesting depth accepted by decode() and produced by encode(). The default is 512. Deeply nested input would otherwise exhaust the Perl stack, so the limit matters when decoding untrusted data. Setting the variable to a false value disables the check entirely.

The limit counts objects and arrays, not values, and both directions stop at the same number of them, so any document decode() accepts can be handed straight back to encode().

local $mb::JSON::MAX_DEPTH = 64;
my $data = mb::JSON::decode($untrusted);

$mb::JSON::STRICT

When false (the default), decode() copies string bytes through unchanged: raw control characters are accepted and UTF-8 is not validated. This keeps the module usable as a byte-transparent filter.

When true, decode() rejects a raw control character U+0000-U+001F inside a string, and rejects a string whose bytes are not well-formed UTF-8.

local $mb::JSON::STRICT = 1;
my $data = mb::JSON::decode($untrusted);

BOOLEAN VALUES

Perl has no native boolean type. To represent JSON true and false unambiguously, mb::JSON provides two singleton objects:

mb::JSON::true   -- stringifies as "true",  numifies as 1
mb::JSON::false  -- stringifies as "false", numifies as 0

Use these when encoding a boolean value:

my $json = mb::JSON::encode({ flag => mb::JSON::true });
# -> '{"flag":true}'

A plain 1 or 0 encodes as a JSON number, not a boolean:

my $json = mb::JSON::encode({ count => 1 });
# -> '{"count":1}'

When decoding, JSON true and false are returned as mb::JSON::Boolean objects, which behave as 1 and 0 in numeric and boolean context.

stringify() behaves identically to encode() for boolean values.

ENCODING RULES

Applies to both encode() and stringify().

undef -> null
mb::JSON::true -> true, mb::JSON::false -> false
Number

A scalar matching the JSON number pattern is encoded as a bare number. Inf and NaN have no JSON representation and are rejected with an error rather than silently emitted as the strings "Inf" and "NaN".

String

Encoded as a double-quoted JSON string. UTF-8 multibyte bytes are output as-is (not \uXXXX-escaped). Control characters U+0000-U+001F are escaped.

ARRAY reference -> JSON array [...]
HASH reference -> JSON object {...}

Hash keys are sorted alphabetically for deterministic output.

Nesting

A structure nested deeper than $mb::JSON::MAX_DEPTH is rejected, and a reference that contains itself is reported as a circular reference instead of recursing forever.

DECODING RULES

Applies to both decode() and parse().

null -> undef
true -> mb::JSON::Boolean (numifies to 1)
false -> mb::JSON::Boolean (numifies to 0)
Number -> Perl number
String -> Perl string (\uXXXX converted to UTF-8)

A \uXXXX escape in the Basic Multilingual Plane is converted to its UTF-8 byte sequence. A UTF-16 surrogate pair (\uD800-\uDBFF followed by \uDC00-\uDFFF) is combined into the single code point it represents and emitted as 4-byte UTF-8, so characters above U+FFFF (e.g. emoji) decode correctly.

Object -> hash reference

When the same key appears more than once, the last occurrence wins.

Array -> array reference
Byte order mark

A leading UTF-8 byte order mark (EF BB BF) is skipped before parsing.

Whitespace

Space, tab, line feed and carriage return are allowed between tokens, which is exactly the set RFC 8259 defines. Form feed and vertical tab are not whitespace and are reported as an unexpected token. The set is spelled out rather than written as perl's \s, so it is the same on every supported perl; \s matches form feed everywhere and, from perl 5.18, vertical tab as well.

Nesting

Input nested deeper than $mb::JSON::MAX_DEPTH is rejected. The limit counts containers, so a scalar inside the deepest allowed object or array is not a further level. encode() applies the same rule, and therefore can always write back out whatever decode() accepted.

LIMITATIONS

  • References other than ARRAY and HASH (e.g. code references, blessed objects other than mb::JSON::Boolean) are stringified rather than raising an error. A blessed hash or array reference is stringified too, because ref() returns the class name rather than HASH or ARRAY.

  • A scalar that matches the JSON number pattern is encoded as a number even when it was intended as a string, so "30" becomes 30. Leading-zero strings such as "007" are preserved as strings because they do not match the JSON number pattern.

  • Numbers are decoded through Perl's own numeric conversion, so an integer wider than the platform's floating point mantissa loses precision, and 1.0 or 1e2 is re-encoded as 1 or 100.

  • By default the decoder does not validate UTF-8 and accepts raw control characters inside strings. Set $mb::JSON::STRICT to enable those checks.

  • Inf and NaN are recognised by how they print, so a string that spells exactly "Inf", "-Inf", "Infinity", "NaN" or one of the "1.#INF" forms is rejected as well, even when it was meant as text. Longer words are unaffected: "Info" and "nano" encode normally. Perl offers no portable way to tell a numeric Inf from a string spelling it -- from perl 5.22 even "Info" numifies to Inf -- so the module prefers a rule that behaves identically on every supported perl.

DIAGNOSTICS

mb::JSON::decode: unexpected end of input

The JSON text ended before a complete value was parsed.

mb::JSON::decode: unexpected token: <text>

An unrecognised token was encountered while parsing.

mb::JSON::decode: expected string key in object

An object key was not a quoted string.

mb::JSON::decode: expected ':' after key '<key>'

The colon separator was missing after an object key.

mb::JSON::decode: expected ',' or '}' in object

A JSON object was not properly terminated or separated.

mb::JSON::decode: expected ',' or ']' in array

A JSON array was not properly terminated or separated.

mb::JSON::decode: unterminated string

A JSON string was not closed with a double-quote.

mb::JSON::decode: invalid escape sequence: <text>

A backslash inside a string was followed by a character that is not a recognized escape (\", \\, /, \b, \f, \n, \r, \t) or a \uXXXX sequence.

mb::JSON::decode: trailing garbage: <text>

Extra text was found after a successfully parsed top-level value.

mb::JSON::decode: lone high surrogate in Unicode escape sequence

A high surrogate \uD800-\uDBFF was not followed by a \uXXXX escape.

mb::JSON::decode: invalid low surrogate in Unicode escape sequence

A high surrogate was followed by a \uXXXX escape that was not a low surrogate \uDC00-\uDFFF.

mb::JSON::decode: lone low surrogate in Unicode escape sequence

A low surrogate \uDC00-\uDFFF appeared without a preceding high surrogate.

mb::JSON::decode: raw control character in string

$mb::JSON::STRICT is set and an unescaped character in the range U+0000-U+001F appeared inside a string.

mb::JSON::decode: malformed UTF-8 in string

$mb::JSON::STRICT is set and a decoded string is not a well-formed UTF-8 byte sequence.

mb::JSON::decode: number out of range: <text>

A JSON number literal is finite JSON syntax but outside the range of this perl's floating point type. Too large and converting it would silently produce Inf, -Inf, or NaN; too small and it would silently produce 0, having lost every significant digit the literal carried. decode() rejects both instead, mirroring encode()'s refusal to write a non-finite value out (see mb::JSON::encode: cannot encode Inf or NaN above). Where that range ends depends on how perl was built: an ordinary double runs out around 1e308, so 1e400 and 1e-400 are rejected there, while a perl built -Duselongdouble or -Dusequadmath accepts both and rejects only far more extreme literals. A literal that is genuinely zero is unaffected on every build: only the mantissa is examined, so 0, 0.000 and 0e-400 all decode to 0.

mb::JSON::decode: nesting too deep (max <N>)

The JSON text nests objects or arrays deeper than $mb::JSON::MAX_DEPTH.

mb::JSON::encode: nesting too deep (max <N>)

The Perl data structure nests deeper than $mb::JSON::MAX_DEPTH.

mb::JSON::encode: circular reference detected

The Perl data structure contains a reference to one of its own ancestors, which has no JSON representation.

mb::JSON::encode: cannot encode Inf or NaN

A numeric value was Inf, -Inf, or NaN. JSON has no syntax for these; encode them as null or as a string yourself if you need them.

INCOMPATIBLE CHANGES

These changes in 0.07 can alter the behaviour of code written against 0.06.

  • decode()/parse() reject an unrecognized backslash escape inside a string with mb::JSON::decode: invalid escape sequence. Earlier releases passed the backslash through literally.

  • encode() croaks on Inf/NaN, and on a string spelling one of them exactly, rather than emitting "Inf" or "NaN" as a JSON string. See "LIMITATIONS".

  • encode() croaks on a circular reference instead of recursing until the process runs out of stack.

  • decode() and encode() croak on nesting deeper than $mb::JSON::MAX_DEPTH, which defaults to 512. Earlier releases had no limit.

  • decode() skips a leading UTF-8 byte order mark instead of reporting an unexpected token.

  • encode() called with no argument at all encodes $_ instead of returning null. An explicit encode(undef) still returns null.

  • decode()/parse() croak on a number literal outside perl's floating point range instead of silently converting it. Too large (1e400) used to return Inf, which encode() would then refuse to re-encode; too small (1e-400) used to return 0.

  • decode()/parse() accept only the four whitespace bytes RFC 8259 allows between tokens. Earlier releases used perl's \s, which also accepted form feed on every perl and vertical tab on perl 5.18 and later -- so a document containing one of those decoded on some perls and not on others.

  • encode()/stringify() apply $mb::JSON::MAX_DEPTH to objects and arrays only, and stop at the same count decode() does. Earlier releases counted leaf scalars as a level of their own and allowed one container more than decode() would read back.

PERFORMANCE

Up to 0.06 the decoder consumed its input by repeatedly deleting the front of the buffer, which copies the remainder of the text on every token and makes decoding quadratic in the input length. Since 0.07 the scanner advances pos() instead, so decoding is linear. On one reference machine an 800 KB document took over 12 seconds to decode with the old scanner and about 0.2 seconds with the new one. Documents of a few kilobytes are too small for the difference to matter.

parse() and stringify() hand their argument straight to decode() and encode(). Up to 0.06 each copied it into a lexical first, so calling the alias rather than the primary name duplicated the whole document for nothing.

encode() is roughly 20% slower than before. That is the cost of tracking the current path so that a circular reference can be reported instead of recursing until the process runs out of stack.

SEE ALSO

JSON::PP, JSON::XS

BUGS

Please report bugs to ina.cpan@gmail.com.

AUTHOR

INABA Hitoshi <ina.cpan@gmail.com>

COPYRIGHT AND LICENSE

Copyright (c) 2021, 2022, 2026 INABA Hitoshi <ina.cpan@gmail.com>

This software is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See perlartistic.