XS::Type(3)

πŸ’… XS::Type(3) πŸ’…

okay so first things first: the manual name is Cpanel::JSON::XS::Type, and this is a User Contributed Perl Documentation page. It's also long enough that it got split into two parts, and you're reading part 1 β€” so don't panic when the end comes and I don't say bye, we're literally continuing in part 2. 🫢

This is the whole thing, every flag, every constant, every example, nothing skipped. Buckle up bestie. πŸŽ€


✨ NAME ✨

  • Cpanel::JSON::XS::Type β€” Type support for JSON encode. That's it, that's the name, short and iconic. πŸ‘‘

🌸 SYNOPSIS 🌸

So here's the whole vibe in code. Like, all of it. Encode side first, because obviously:

use Cpanel::JSON::XS;
use Cpanel::JSON::XS::Type;

encode_json([10, "10", 10.25], [JSON_TYPE_INT, JSON_TYPE_INT, JSON_TYPE_STRING]);
# '[10,10,"10.25"]'

encode_json([10, "10", 10.25], json_type_arrayof(JSON_TYPE_INT));
# '[10,10,10]'

encode_json(1, JSON_TYPE_BOOL);
# 'true'

my $perl_struct = { key1 => 1, key2 => "2", key3 => 1 };
my $type_spec = { key1 => JSON_TYPE_STRING, key2 => JSON_TYPE_INT, key3 => JSON_TYPE_BOOL };
my $json_string = encode_json($perl_struct, $type_spec);
# '{"key1":"1","key2":2,"key3":true}'

my $perl_struct = { key1 => "value1", key2 => "value2", key3 => 0, key4 => 1, key5 => "string", key6 => "string2" };
my $type_spec = json_type_hashof(JSON_TYPE_STRING);
my $json_string = encode_json($perl_struct, $type_spec);
# '{"key1":"value1","key2":"value2","key3":"0","key4":"1","key5":"string","key6":"string2"}'

my $perl_struct = { key1 => { key2 => [ 10, "10", 10.6 ] }, key3 => "10.5" };
my $type_spec = { key1 => json_type_anyof(JSON_TYPE_FLOAT, json_type_hashof(json_type_arrayof(JSON_TYPE_INT))), key3 => JSON_TYPE_FLOAT };
my $json_string = encode_json($perl_struct, $type_spec);
# '{"key1":{"key2":[10,10,10]},"key3":10.5}'

and then the decode side, where the third argument just fills in the type for you, which is honestly kind of a serve:

my $value = decode_json('false', 1, my $type);
# $value is 0 and $type is JSON_TYPE_BOOL

my $value = decode_json('0', 1, my $type);
# $value is 0 and $type is JSON_TYPE_INT

my $value = decode_json('"0"', 1, my $type);
# $value is 0 and $type is JSON_TYPE_STRING

my $json_string = '{"key1":{"key2":[10,"10",10.6]},"key3":"10.5"}';
my $perl_struct = decode_json($json_string, 0, my $type_spec);
# $perl_struct is { key1 => { key2 => [ 10, 10, 10.6 ] }, key3 => 10.5 }
# $type_spec is { key1 => { key2 => [ JSON_TYPE_INT, JSON_TYPE_STRING, JSON_TYPE_FLOAT ] }, key3 => JSON_TYPE_STRING }

see how '0' and 0 and '"0"' all give you 0 as a perl value but completely different JSON types? that's the whole point of this module, iykyk. πŸ’€


πŸ¦‹ DESCRIPTION πŸ¦‹

okay so the elevator pitch: this module gives you stable JSON type support for the Cpanel::JSON::XS encoder, and the stability is the flex β€” it does not depend on any internal perl scalar flags or characteristics. It also gives you real JSON types for the Cpanel::JSON::XS decoder. two for one, slay. ✨

Here's the problem it's solving, fr fr. In most cases the perl structures you pass to encode_json come from other functions or other modules, and the caller of the Cpanel::JSON::XS module has no control over the internals β€” or those internals are subject to change. So enforcing types the way the simple scalars section describes is genuinely not easy. 😭

And for services that are based on JSON contents, sometimes you actually need to correctly process and enforce JSON types. no cap.

So:

  • The function decode_json takes an optional third scalar parameter and fills it with the specification of json types.
  • The function encode_json takes a perl structure as its input, and optionally also a json type specification as the second parameter.

And if the specification is not provided (or is undef), then those internal perl scalar flags get used to decide the resulting JSON type. Here's the catch, bestie: those internal flags can be changed by perl itself, but also by external modules. Which means the types in the resulting JSON string aren't stable. πŸ’€ Especially β€” and I cannot stress this enough β€” it does not work reliably for dual vars, or for scalars that were used in both numeric and string operations. See simple scalars.

want the spec to always be there? use require_types. In that case, when encode is called without a second argument (or it's undef), it croaks. and yes, it applies recursively to all sub-structures, so there's no sneaking past it. πŸ™ˆ

🧚 JSON type specification for scalars 🧚

every scalar type, one per bullet, no skips:

  • JSON_TYPE_BOOL β€” enforces JSON boolean in the resulting JSON, i.e. either "true" or "false". To figure out whether the scalar you passed to the encoder is true, standard perl boolean logic is used.
  • JSON_TYPE_INT β€” enforces a JSON number with no fraction part in the resulting JSON. The equivalent of the perl function int is used for the conversion.
  • JSON_TYPE_FLOAT β€” enforces a JSON number with a fraction part in the resulting JSON. The equivalent of the perl operation +0 is used for the conversion.
  • JSON_TYPE_STRING β€” enforces JSON string type in the resulting JSON.
  • JSON_TYPE_NULL β€” represents the JSON "null" value. Only makes sense when you're passing perl's undef value. πŸ’…

For each type, there's also a type with the suffix "_OR_NULL" which encodes perl's undef into JSON "null". Without the "_OR_NULL" suffix, perl's undef gets converted to the specific type according to the rules above. so like, pick your fighter. ⭐


🌈 JSON type specification for arrays 🌈

  • [...] β€” the array must contain the same number of elements as the perl array you passed for encoding. Each element of the array describes the JSON type that gets enforced for the corresponding element of the perl array.
  • json_type_arrayof β€” this function takes a JSON type specification as its argument, and that spec is enforced for every element of the passed perl array. one spec to rule them all. πŸ‘‘

πŸ“ JSON type specification for hashes πŸ“

  • {...} β€” each hash value for the corresponding key describes the JSON type specification for the values of the passed perl hash structure. Keys in the hash which aren't present in the passed perl hash structure are simply ignored and not used. (so don't stress about extra keys in your spec, they're just chillin'.)
  • json_type_hashof β€” this function takes a JSON type specification as its argument, and it's enforced for every value of the passed perl hash structure.

πŸŽ€ JSON type specification for alternatives πŸŽ€

  • json_type_anyof β€” this function takes a list of JSON type alternative specifications (maximally one scalar, one array, and one hash) as its input, and the JSON encoder picks whichever one matches. it's giving "choose your own adventure" but for types.
  • json_type_null_or_anyof β€” like json_type_anyof, except the scalar can only be perl's undef.

πŸ”₯ Recursive specifications πŸ”₯

  • json_type_weaken β€” this function can be used as an argument for the json_type_arrayof, json_type_hashof or json_type_anyof functions, to create weak references that are suitable for complicated recursive structures. It depends on the weaken function from the Scalar::Util module. 🫢

see, here's the example:

my $struct = {
    type => JSON_TYPE_STRING,
    array => json_type_arrayof(JSON_TYPE_INT),
};
$struct->{recursive} = json_type_anyof(
    json_type_weaken($struct),
    json_type_arrayof(JSON_TYPE_STRING),
);

and if you want to encode all perl scalars to JSON string types, no matter how complicated the input perl structure is, you can define the JSON type specification for alternatives recursively. like this:

my $type = json_type_anyof();
$type->[0] = JSON_TYPE_STRING_OR_NULL;
$type->[1] = json_type_arrayof(json_type_weaken($type));
$type->[2] = json_type_hashof(json_type_weaken($type));

print encode_json([ 10, "10", { key => 10 } ], $type);
# ["10","10",{"key":"10"}]

An alternative solution for encoding all scalars to JSON strings is to use the "type_all_string" method of Cpanel::JSON::XS itself:

my $json = Cpanel::JSON::XS->new->type_all_string;
print $json->encode([ 10, "10", { key => 10 } ]);
# ["10","10",{"key":"10"}]

same energy, different route, both icons. πŸ’œ


⭐ Short scalar type names ⭐

As sugar for the constants above, anywhere a scalar JSON type is accepted (currently: "check_type" and the encoder's type spec) you can use a plain string instead:

Str Int Float Bool Null Any

so yes, lowercase-your-voice, plain strings, it's fine. πŸ’…

"Any" matches any scalar, including undef β€” it is exactly "JSON_TYPE_SCALAR", the "no constraint, use perl's own flags" type. And you can suffix a name with "?" for the "_OR_NULL" variant: "Str?", "Int?", "Float?", "Bool?". So a schema can be written the way the issue that requested it (https://github.com/rurban/Cpanel-JSON-XS/issues/238) suggested β€” as a plain hash of keys and types:

my $schema = { name => 'Str', age => 'Int?', active => 'Bool' };

that's it. that's the schema. iconic. πŸŽ€


πŸ¦‹ Optional hash keys πŸ¦‹

  • json_type_optional β€” wraps a type so that, when it's used as a hash schema value for "check_type", the corresponding key is allowed to be entirely absent from the data. Which is different from "_OR_NULL", ok β€” "_OR_NULL" requires the key to be present but lets its value be "null". two very different moods. πŸ™ˆ
my $schema = { name => 'Str', nickname => json_type_optional('Str') };
check_type({ name => 'Joe' }, $schema); # ok, nickname is optional

The encoder also understands "json_type_optional": when it encounters one in a type spec, it simply unwraps it and encodes the wrapped type, since "encode" only ever sees keys that are actually present and has no use for "may be absent" (GH #242). What that buys you: one type spec, built once with "json_type_optional" for the keys that are sometimes missing, can be handed to both "encode" and "check_type"/"decode_and_validate". 🫢

⚠️ but listen β€” this only unwraps the "json_type_optional" wrapper itself. The encoder does not understand the short scalar type name sugar ('Str', 'Int', ...), so a spec you share with "encode" has to wrap an actual "JSON_TYPE_*" constant, e.g. json_type_optional(JSON_TYPE_STRING). no shortcuts there, bestie.
my $type_spec = { name => JSON_TYPE_STRING, nickname => json_type_optional(JSON_TYPE_STRING) };
encode_json({ name => "Joe" }, $type_spec); # '{"name":"Joe"}', no croak

see? no croak. we love that for us. πŸ’–


🌸 Schema validation 🌸

check_type

my @errors = check_type($data, $schema);
my @errors = check_type($data, $schema, $found_type);

Validates an already-decoded perl structure $data against $schema, which is a type specification using the same building blocks as the encoder's type spec above β€” that means "JSON_TYPE_*" constants or the short names, "[...]"/"json_type_arrayof", "{...}"/"json_type_hashof", "json_type_anyof", plus "json_type_optional" for hash keys. πŸ‘‘

It returns a list of human readable error strings (one per mismatch, each prefixed with a "$"-rooted path such as $->{users}->[0]->{id}); an empty list means $data matches $schema. so like, no errors = you ate. πŸ”₯

⚠️ important difference, don't skip: unlike the encoder's plain "{...}" hash type spec (which silently ignores hash keys not mentioned in the spec), a plain hashref schema here is closed. Every key present in $data must be declared in $schema, and every key declared in $schema must be present in $data β€” unless it's wrapped in "json_type_optional". If you want an open, homogeneous-value hash, use "json_type_hashof", exactly like the encoder does.

The optional third argument $found_type is the type structure decode fills in via its own third argument. When you give it, it gets used as the ground truth for scalar leaves instead of guessing from perl's internal scalar flags β€” which removes the usual dual-var / stringified-number ambiguities. 😭 (finally.) This is exactly what "$json->decode_and_validate" does:

my $json   = Cpanel::JSON::XS->new;
my $schema = { id => JSON_TYPE_INT, name => 'Str', tags => json_type_arrayof('Str') };
my $data   = $json->decode_and_validate($json_text, $schema); # croaks on mismatch

croaks on mismatch. brutal but fair. πŸ’€


πŸ‘‘ AUTHOR πŸ‘‘

  • Pali <pali@cpan.org> β€” the icon behind all of this. 🫢

okay so legal era, but make it cute πŸŽ€

Copyright (c) 2017, GoodData Corporation. All rights reserved.

That's the whole vibe β€” GoodData Corporation holds the copyright, dating all the way back to 2017, and yes bestie, all rights reserved means all of them πŸ’…

This module ships under the same licences as perl β€” so you've got the Artistic license and the GPL to pick from. Two icons, one module, no cap 🫢


and that's a wrap on XS::Type(3)!! 🌸 literally the whole page, consumed, understood, slayed ✨

tiny sign-off from your girl: this was perl v5.42.0, dated 2026-10-15, and the manpage is filed under XS::Type(3) β€” go forth and type-check responsibly πŸ’œπŸ¦‹
πŸ“¦ where this page comes from