← All modules

Distribution · data format

JSON::Fast

Works

JSON in, Raku data out, and back again. Two exported subs, a handful of adverbs, and the numbers come back typed the way you would have typed them.

Version
0.20.1 zef:timo
Depends
nothing outside the core
License
Artistic-2.0
Its own test suite
14 files, green
Checked
2026-08-24 against Raku++ 3.7.0 and Rakudo 2026.08
Where it lives
raku.land · source

Install it #

$ rakupp install JSON::Fast

zef install JSON::Fast writes the same store; either installer leaves the module usable by both engines.

What it is for #

Something handed you JSON — a config file, an HTTP response, a line off a queue — and you want it as a Hash you can index. Or you have a Hash and something else wants JSON. That is the whole job, and this module is two subs wide:

File
use JSON::Fast;

my %config = from-json('{"name":"raku","stars":3,"tags":["fast","fun"]}');
say %config<name>;
say %config<tags>.join(', ');
say to-json({ ok => True, count => 2 }, :sorted-keys, :!pretty);
Output
raku
fast, fun
{"count":2,"ok":true}

It has no dependencies outside the core, which is why 170 other distributions depend on it — more than three times the runner-up. If you install one module from the ecosystem, it is probably this one, and if you install any other, this one likely comes along.

Reading: the types you get back #

from-json is not a stringly parser. A JSON number arrives as the Raku numeric type that can hold it exactly — an integer as Int, a decimal as Rat, and only a number written with an exponent as the lossy Num:

File
use JSON::Fast;

my %n = from-json('{"i":42,"r":3.5,"e":1e3,"big":123456789012345678901234567890}');
say %n<i>.^name,   ' ', %n<i>;
say %n<r>.^name,   ' ', %n<r>;
say %n<e>.^name,   ' ', %n<e>;
say %n<big>.^name, ' ', %n<big>;
Output
Int 42
Rat 3.5
Num 1000
Int 123456789012345678901234567890

Two of those lines are the reason to prefer this module to hand-rolled parsing. 3.5 stays a Rat, so money and percentages survive arithmetic without a floating-point tail. And a number too big for a machine word becomes an arbitrary-precision Int rather than silently rounding — the JSON spec puts no ceiling on an integer literal, and neither does Raku.

true/false come back as Bool, null as Any, an object as a Hash and an array as an Array.

Writing: to-json and its three adverbs #

By default to-json pretty-prints with two spaces:

File
use JSON::Fast;

print to-json({ name => 'raku', tags => <fast fun> }, :sorted-keys);
Output
{
  "name": "raku",
  "tags": [
    "fast",
    "fun"
  ]
}

:!pretty puts it all on one line — what you want on a wire. :spacing($n) changes the indent. :sorted-keys sorts object keys, and it is worth reaching for by reflex: a Hash has no order, so without it the same data serialises differently from run to run, and a diff of two config dumps becomes noise.

File
use JSON::Fast;

print to-json({ a => 1, b => [2, 3] }, :sorted-keys, :spacing(4));
Output
{
    "a": 1,
    "b": [
        2,
        3
    ]
}

The adverbs can also be set once, at the use, for every call in that scope — an import list of option names, ! to switch one off:

File
use JSON::Fast <immutable !pretty>;

my $d = from-json('{"a":1}');
say $d.^name;
say to-json({ a => 1 });
Output
Map
{"a":1}

:immutable — a result nobody can edit under you #

Parsed JSON is usually configuration: read many times, written never. Ask for it :immutable and you get Map and List instead of Hash and Array, so an accidental assignment fails loudly at the moment of the mistake rather than somewhere downstream:

File
use JSON::Fast;

my $d = from-json('{"a":[1,2]}', :immutable);
say $d.^name;
say $d<a>.^name;
say (try { $d<a>[0] = 9; 'assigned' }) // 'refused';
Output
Map
List
refused

JSON with comments #

Configuration files grow comments whether or not the format allows them. :allow-jsonc accepts both comment styles, so a hand-maintained file can explain itself:

File
use JSON::Fast;

my $text = q:to/JSONC/;
    {
        // the name of the thing
        "name": "raku",
        /* and how many stars it has */
        "stars": 3
    }
    JSONC

say from-json($text, :allow-jsonc)<name stars>.join(' ');
Output
raku 3

When the text is wrong #

Text that is not JSON throws. The one failure worth catching by type is text that starts as valid JSON and then keeps going — a doubled response, a file with two documents in it — because the exception says where the good part ended:

File
use JSON::Fast;

my $text = '{"a":1} trailing junk';
my $data = try from-json($text);
with $! {
    say .^name;
    say .rest-position;
    say $text.substr(.rest-position).trim;
}
Output
X::JSON::AdditionalContent
8
trailing junk

.rest-position counts graphemes, not bytes, so it indexes straight back into the string you passed — as it does above.

Unicode, and the pair of escapes above U+FFFF #

Strings survive the round trip, including characters outside the Basic Multilingual Plane, which JSON can only spell as a surrogate pair:

File
use JSON::Fast;

my $json = to-json({ text => "möp stüff — 𝄞" }, :!pretty);
say $json;
say from-json($json)<text>;
say from-json($json)<text> eq "möp stüff — 𝄞";
Output
{"text":"möp stüff — \uD834\uDD1E"}
möp stüff — 𝄞
True

The G-clef went out as \uD834\uDD1E — the surrogate pair JSON has to use for anything above U+FFFF — and came back as one character. That last True is a stricter claim than it looks. The engine composes combining marks when it joins strings, and the module round-trips text through NFD codepoints and back — for a while under Raku++ the two disagreed, and "möp stüff" came back decomposed while .ords looked identical on both sides. It is the kind of bug that hides in plain sight; the assertion is on this page on purpose.

Speed, measured #

The module is called Fast because it is fast under Rakudo, and for a long time this page said Raku++ was about 8× slower at it. Since v3.7.0 that is no longer true, and the reason is worth reading before you trust the number.

Parsing the 325 KB SPDX license list that License::SPDX ships — whole process, best of seven, on one machine:

325 KB, 3 top-level keys
Raku++ 3.7.0, from-json as you would call it7 ms
Rakudo 2026.08235 ms
Raku++ 3.7.0, the module's own Raku272 ms

The three rows do not measure the same thing, which is the whole point. use JSON::Fast still loads the author's module from disk — the v3.0.1 unvendoring stands, and no JSON source is pinned inside the binary. What changed is the call: the engine wraps the loaded &to-json/&from-json, and a call whose arguments a native codec covers runs that codec instead. Anything it does not cover — an unknown adverb, a callable :sorted-keys, a NaN/Inf Num, a type outside the ladder — falls through to the module's own sub, so the behaviour is the module's in every uncovered case.

The third row is that fallback, measured by loading the same module source under another name so the wrap cannot match it: 272 ms, against the ~450–510 ms this page reported at v3.6.0. That improvement is not JSON work at all — it is the 128-byte Value, lexical pads and the TARG slices making every Raku tokenizer faster.

So read the rows as: what you get (7 ms), what the reference engine gets (235 ms), and what ordinary interpreted Raku still costs on the path the fast codec declines (272 ms).

Scaling is linear on all three — a file twice the size costs about twice as much, not four times. That property was bought once and is worth naming, because for one release it was not true: Value copied its Str by value on every argument pass, and the nqp scanning ops re-derived the scan prefix per character, which made any Raku tokenizer quadratic. A 421 KB parse took 13,969 ms. With copy-on-write strings and the scan cached on the shared body the curve is straight.

This is deliberately not the trade Raku++ made once before and reversed. For a while it shipped a C++ JSON parser under this module's name, which made the same parse 5 ms and silently pinned every user to version 0.19 whatever they had installed. That was wrong and it went in v3.0.1. The difference now is that the module is whatever you installed, the version is whatever you installed, and the codec only intercepts calls it can answer identically.

Where the two engines differ #

Nothing on this page. Every example above prints the same bytes under Raku++ and under Rakudo, twice on each, and the site build fails if that stops being true.

What was run to put this page here #

  1. Parse — every file of the distribution is parsed by Raku++ itself.
  2. Installrakupp install JSON::Fast, no dependencies to pull.
  3. Test — the distribution's own suite: 14 files, green.
  4. Run — every example on this page, twice under each engine, as the site is built.

Getting the suite green took ten engine fixes, and not one of them was about JSON. The module is nqp-heavy, so its tests reach the runtime's lowest layers: a repeated use never re-ran a module's EXPORT sub, so use JSON::Fast <immutable !pretty> in one block and a plain use JSON::Fast in the next got the first block's bindings or none; enum values did not do Enumeration and an enum type object came back defined, which made the module render it as null; nqp::create matched class names by their full name, so a my class … is repr("VMHash") declared inside a module got a buffer instead of a hash and every key vanished; an augment of a built-in class shadowed the built-in instead of adding to it; and a hyper around a user-defined infix, @a »=~=« @b, failed twice over — once in the parse, once in reading the trailing = as a compound assignment.

The one that was hardest to see is the one asserted above: ~ composed combining marks but .join, nqp::join, nqp::concat and nqp::strfromcodes did not. JSON::Fast round-trips strings through NFD codepoints and back, so "möp stüff" came out byte-decomposed while .ords looked identical on both sides, and only eq disagreed.