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.