NAME

Cpanel::JSON::XS::Type - Type support for JSON encode

SYNOPSIS

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}'


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 }

DESCRIPTION

This module provides stable JSON type support for the Cpanel::JSON::XS encoder which doesn't depend on any internal perl scalar flags or characteristics. Also it provides real JSON types for Cpanel::JSON::XS decoder.

In most cases perl structures passed to encode_json come from other functions or from other modules and caller of Cpanel::JSON::XS module does not have control of internals or they are subject of change. So it is not easy to support enforcing types as described in the simple scalars section.

For services based on JSON contents it is sometimes needed to correctly process and enforce JSON types.

The function decode_json takes optional third scalar parameter and fills it with specification of json types.

The function encode_json takes a perl structure as its input and optionally also a json type specification in the second parameter.

If the specification is not provided (or is undef) internal perl scalar flags are used for the resulting JSON type. The internal flags can be changed by perl itself, but also by external modules. Which means that types in resulting JSON string aren't stable. Specially it does not work reliable for dual vars and scalars which were used in both numeric and string operations. See simple scalars.

To enforce that specification is always provided use require_types. In this case when encode is called without second argument (or is undef) then it croaks. It applies recursively for all sub-structures.

JSON type specification for scalars:

JSON_TYPE_BOOL

It enforces JSON boolean in resulting JSON, i.e. either true or false. For determining whether the scalar passed to the encoder is true, standard perl boolean logic is used.

JSON_TYPE_INT

It enforces JSON number without fraction part in the resulting JSON. Equivalent of perl function int is used for conversion.

JSON_TYPE_FLOAT

It enforces JSON number with fraction part in the resulting JSON. Equivalent of perl operation +0 is used for conversion.

JSON_TYPE_STRING

It enforces JSON string type in the resulting JSON.

JSON_TYPE_NULL

It represents JSON null value. Makes sense only when passing perl's undef value.

For each type, there also exists a type with the suffix _OR_NULL which encodes perl's undef into JSON null. Without type with suffix _OR_NULL perl's undef is converted to specific type according to above rules.

JSON type specification for arrays:

[...]

The array must contain the same number of elements as in the perl array passed for encoding. Each element of the array describes the JSON type which is enforced for the corresponding element of the perl array.

json_type_arrayof

This function takes a JSON type specification as its argument which is enforced for every element of the passed perl array.

JSON type specification for hashes:

{...}

Each hash value for corresponding key describes the JSON type specification for values of passed perl hash structure. Keys in hash which are not present in passed perl hash structure are simple ignored and not used.

json_type_hashof

This function takes a JSON type specification as its argument which is enforced for every value of 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 chooses one that matches.

json_type_null_or_anyof

Like json_type_anyof, but scalar can be only perl's undef.

Recursive specifications

json_type_weaken

This function can be used as an argument for "json_type_arrayof", "json_type_hashof" or "json_type_anyof" functions to create weak references suitable for complicated recursive structures. It depends on the weaken function from Scalar::Util module. See following 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),
);

If you want to encode all perl scalars to JSON string types despite how complicated is input perl structure you can define JSON type specification for alternatives recursively. It could be defined as:

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 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"}]

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) a plain string may be used instead:

Str Int Float Bool Null Any

Any matches any scalar, including undef (it is exactly JSON_TYPE_SCALAR, the "no constraint, use perl's own flags" type). 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 suggested, as a plain hash of keys and types:

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

Optional hash keys

json_type_optional

Wraps a type so that, when used as a hash schema value for "check_type", the corresponding key is allowed to be entirely absent from the data (as opposed to _OR_NULL, which requires the key to be present but allows its value to be null).

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). This lets one type spec, built once with json_type_optional for the keys that are sometimes missing, be handed to both encode and "check_type"/decode_and_validate. Note that 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 shared with encode must wrap an actual JSON_TYPE_* constant, e.g. json_type_optional(JSON_TYPE_STRING).

my $type_spec = { name => JSON_TYPE_STRING, nickname => json_type_optional(JSON_TYPE_STRING) };
encode_json({ name => "Joe" }, $type_spec); # '{"name":"Joe"}', no croak

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, a type specification using the same building blocks as the encoder's type spec above (JSON_TYPE_* constants or the short names, [...]/json_type_arrayof, {...}/json_type_hashof, json_type_anyof, plus json_type_optional for hash keys). Returns a list of human readable error strings (one per mismatch, prefixed with a $-rooted path such as $->{users}->[0]->{id}); an empty list means $data matches $schema.

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 wrapped in json_type_optional. Use json_type_hashof for an open, homogeneous-value hash, exactly like the encoder.

The optional third argument $found_type is the type structure decode fills in via its own third argument. When given, it is 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. 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

AUTHOR

Pali <pali@cpan.org>

COPYRIGHT & LICENSE

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

This module is available under the same licences as perl, the Artistic license and the GPL.