NAME
Skilletor::JSON - JSON read and written exactly as the reference implementation does
VERSION
version 0.001
SYNOPSIS
use Skilletor::JSON qw( js_parse js_stringify js_ordered );
my $config = js_parse( $text ); # objects keep their key order
$config->{sources}{shared} = js_ordered( git => 'https://github.com/Getty/skills' );
my $out = js_stringify( $config, 2 )."\n"; # JSON.stringify(config, null, 2) + "\n"
DESCRIPTION
The reference implementation reads every file with JSON.parse and writes every file with JSON.stringify(value, null, 2); a lock file, trust.json or an edited config written by either implementation must be the same bytes. JSON::MaybeXS cannot give that: its pretty printer indents by three, sorts or scrambles keys, and escapes differently. This module is the JavaScript behaviour, in Perl.
Strings in and out are character strings; decode and encode UTF-8 at the file boundary ("read_text" in Skilletor::FSUtil).
js_parse
my $value = js_parse($text);
JSON.parse. An object is a hash tied to Skilletor::JSON::OrderedHash (keys in JavaScript property order; a duplicate key keeps its first place and its last value), true/false are JSON::MaybeXS booleans, null is undef, a number is a double as in JavaScript (1e400 is infinity). A text that is no JSON throws Skilletor::Error::JSON with V8's message, position and line/column counted in UTF-16 code units as V8 counts them, e.g. Expected ',' or '}' after property value in JSON at position 6 (line 1 column 7).
js_stringify
my $text = js_stringify( $value ); # JSON.stringify(value)
my $text = js_stringify( $value, 2 ); # JSON.stringify(value, null, 2)
my $text = js_stringify( $value, "\t" ); # JSON.stringify(value, null, "\t")
JSON.stringify, byte for byte. Keys of a hash from "js_parse" or "js_ordered" come in their property order; keys of a plain Perl hash come as the reference writes a sorted object (Object.keys(x).sort(), then JavaScript's reordering of array-index keys first). Strings escape only ", \, control characters and lone surrogates. Numbers are written by "js_number"; a scalar is a number when Perl holds it as one and not as a string (the rule of JSON::PP). undef is null; a blessed object needs a TO_JSON method. No trailing newline: the reference's files add "\n".
js_ordered
my $h = js_ordered( source => 'shared', version => 'git:ab12cd3' );
A hash reference that keeps the given key order, for objects whose key order the reference fixes (a lock entry: source, version, files, …).
js_sort_keys
my @sorted = js_sort_keys(@keys);
Array.prototype.sort without a comparator: by UTF-16 code units, so characters beyond U+FFFF sort before U+E000 .. U+FFFF.
js_number
js_number(0.1 + 0.2); # "0.30000000000000004"
Number.prototype.toString of a double: the shortest digits that read back as the same double, in JavaScript's fixed or exponent form (1e+21, 1e-7); null for NaN and the infinities, as JSON.stringify writes them.
js_type
js_type($value); # 'null', 'boolean', 'number', 'string', 'array' or 'object'
typeof of a parsed value, with null and arrays told apart – the question a validator asks (typeof v !== "string", Array.isArray(v)). An absent key is undefined in JavaScript: test it with exists.
SUPPORT
CONTRIBUTING
Contributions are welcome! Please fork the repository and submit a pull request.
AUTHOR
Torsten Raudssus <getty@cpan.org>
COPYRIGHT AND LICENSE
This software is copyright (c) 2026 by Torsten Raudssus <torsten@raudssus.de> https://raudssus.de/.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.