gem nosj
gem nosj is an very fast JSON parser and generator for Ruby, written in Rust on the first-party nosj crate and SIMD-accelerated on every platform (NEON on Apple Silicon, SSE2/AVX2 on x86-64).
gem nosj is the powerful evil twin of the json gem.
- It is faster than gem json and every third-party parser, including Oj, RapidJSON, FastJsonparser, Yajl. 1.0–1.8× faster than the bundled json gem, 1.3–11× faster than Oj, and up to 17× faster than Yajl—see Benchmarks.
- It comes precompiled (platform gems built with per-platform optimizations, nothing to compile on install).
- It has a partial parsing mode: JSON Pointer lookups that pull single values out of big documents in microseconds, skipping everything else.
- Same API and option names as gem json.
And there's more: validate documents without building a single Ruby object, resolve whole batches of paths in one pass, and accelerate an entire application with a one-line drop-in.
- Requirements
- Getting started
- What's in the box
- Benchmarks
- Switching from the json gem
- How it works
- Development
- License
Requirements
- Ruby 3.3 or newer (CRuby; tested on 3.3, 3.4, and 4.0).
- Linux (x86-64 and arm64, glibc and musl), macOS (Apple Silicon), or Windows (x64): these platforms install precompiled, per-platform-optimized gems with nothing to build.
Getting started
bundle add nosj
require "nosj"
NOSJ.parse('{"a":[1,true]}') #=> {"a" => [1, true]}
NOSJ.generate({"a" => [1, true]}) #=> '{"a":[1,true]}'
That's it—if you know the json gem, you already know nosj.
Want the speedup without touching your code? One line reroutes
JSON.parse, JSON.generate, JSON.pretty_generate, and JSON.dump
through nosj:
require "nosj/json"
In a Bundler app (Rails included) that can live entirely in the Gemfile you can do this:
gem "nosj", require: "nosj/json"
Options nosj supports take the fast path; anything exotic
(create_additions, object_class, JSON::State, procs, IO
arguments) falls back to the original implementation, so JSON.load,
JSON.parse!, and JSON.load_file keep their exact behavior.
Exceptions re-raise as the JSON classes, so your rescue clauses keep
working. Measured through the patch: parse 1.11×, generate 1.05× over
the original gem. (A MultiJson adapter ships too:
require "nosj/multi_json", then MultiJson.use NOSJ::MultiJsonAdapter.)
What's in the box
The json gem API, on the NOSJ module:
NOSJ.parse(src, symbolize_names: true) # also: freeze, max_nesting,
# allow_nan, allow_trailing_comma
NOSJ.generate(obj) # indent, space, object_nl, ...,
NOSJ.pretty_generate(obj) # ascii_only, script_safe, strict
Validation without parsing. NOSJ.valid? runs the full
parser—tokenizers, string decode, number validation—into a null sink
and allocates no Ruby objects at all. It is 2-4× faster than
NOSJ.parse, which already leads every parser above:
NOSJ.valid?('{"a":1}') #=> true
NOSJ.valid?('{"a":}') #=> false
NOSJ.valid?(src, max_nesting: false) # same options as parse
Partial parsing. Pull values out of a document without materializing the rest—skipped content is stepped over at SIMD block speed, so a lookup costs what it skips, not what the document weighs:
NOSJ.dig(json, "users", 3, "name") # Hash#dig-shaped
NOSJ.at_pointer(json, "/users/3/name") # JSON Pointer
# Many lookups in one pass. A batch costs about as much as its
# single deepest member:
NOSJ.at_pointers(json, ["/users/3/name", "/meta/count"])
NOSJ.dig_many(json, [["users", 3, "name"], ["meta", "count"]])
Example: an early field resolves in ~0.35µs where JSON.parse(json).dig(...)
costs ~980µs on the same document—three orders of magnitude. A field
at the far end of a 570 KB document costs ~71µs, still 13× faster
than parse-then-dig. Misses return nil; matched subtrees materialize
with the same options as parse (symbolize_names:, freeze:).
Benchmarks
Every installed JSON gem, benchmark-ips: AWS EC2 c7a.2xlarge (AMD EPYC 9R14, Zen 4), Ruby 4.0.6 + YJIT, json 2.21.1, Oj 3.17.4, RapidJSON 0.4.0, FastJsonparser 0.6.0, Yajl 1.4.3, PGO build, 2026-07-16. ×N = times slower than nosj.
Parse:
| file | nosj (i/s) | json | Oj | FastJsonparser | RapidJSON | Yajl |
|---|---|---|---|---|---|---|
| activitypub | 12.4k | ×1.18 | ×1.53 | ×1.89 | ×1.99 | ×4.57 |
| canada | 248 | ×1.10 | ×8.28 | ×1.45 | ×1.51 | ×4.96 |
| citm_catalog | 504 | ×1.03 | ×1.97 | ×2.07 | ×1.83 | ×4.93 |
| gsoc-2018 | 397 | ×1.30 | ×1.47 | ×1.81 | ×1.80 | ×4.78 |
| homebrew-formula | 15.2 | ×1.13 | ×1.54 | ×2.42 | ×2.02 | ×4.30 |
| homebrew-llvm | 49.3k | ×1.60 | ×1.48 | ×1.50 | ×1.85 | ×5.11 |
| mesh | 1.1k | ×1.30 | ×3.93 | ×1.82 | ×1.95 | ×6.96 |
| numbers | 5.9k | ×1.19 | ×4.64 | ×1.43 | ×1.75 | ×7.04 |
| ohai | 14.4k | ×1.47 | ×1.61 | ×2.11 | ×1.97 | ×4.71 |
| simple | 977k | ×1.31 | ×1.77 | ×2.08 | ×1.63 | ×5.15 |
| small_mixed | 3.2M | ×1.48 | ×2.85 | ×2.44 | ×1.82 | ×6.75 |
| tolstoy | 8.9k | ×1.79 | ×1.96 | ×2.29 | ×2.10 | ×17.31 |
| 1.1k | ×1.09 | ×1.83 | ×2.25 | ×2.61 | ×5.40 |
Generate:
| file | nosj (i/s) | json | Oj | RapidJSON | Yajl |
|---|---|---|---|---|---|
| activitypub | 34.8k | ×1.20 | ×1.78 | ×2.41 | ×5.84 |
| canada | 150 | ×0.98* | ×10.98 | ×11.20 | ×10.87 |
| citm_catalog | 1.3k | ×1.04 | ×1.44 | ×1.55 | ×2.85 |
| gsoc-2018 | 1.1k | ×1.28 | ×2.64 | ×3.64 | ×11.00 |
| homebrew-formula | 20.9 | ×1.07 | ×1.25 | ×1.62 | ×3.03 |
| homebrew-llvm | 71.5k | ×1.23 | ×2.22 | ×3.04 | ×5.80 |
| mesh | 613 | ×1.08 | ×9.24 | ×9.40 | ×9.26 |
| numbers | 2.1k | ×1.03 | ×10.63 | ×11.00 | ×10.69 |
| ohai | 39.4k | ×1.07 | ×1.29 | ×1.50 | ×3.37 |
| simple | 2.2M | ×1.03 | ×1.58 | ×1.62 | ×4.36 |
| small_mixed | 5.6M | ×1.09 | ×2.26 | ×1.83 | ×7.16 |
| tolstoy | 8.3k | ×1.30 | ×4.15 | ×6.50 | ×14.54 |
| 2.9k | ×1.09 | ×1.46 | ×2.01 | ×4.05 |
* canada-generate is a statistical tie with the json gem (within measurement error).
Reproduce with rake bench (the parity-gated comparison, after a PGO retrain—the shipping configuration) or rake bench:ips (the multi-gem shoot-out).
Switching from the json gem
You mostly don't have to do anything. Some differences:
- The legacy object-deserialization options (
create_additions,object_class,array_class,decimal_class) raise ArgumentError; thenosj/jsondrop-in falls back to the original gem for them. - Behaviors the
jsongem itself deprecates (JS comments, raw invalid UTF-8) follow the strict semantics instead. - Unlike
Array#dig, negative indices inNOSJ.digreturn nil (JSON Pointer has no equivalent). - Parse error messages use byte offsets rather than the gem's phrasing (classes match).
Everything else—including the gem's exact float formatting, which is not the shortest-round-trip form most libraries emit—matches byte-for-byte and is verified continuously against the full corpus.
How it works
Most fast parsers build their own tree first and convert it into Ruby objects second, paying for every string and container twice. nosj is built on the nosj Rust crate, an event parser with no tree of its own:
- No intermediate tree. The crate parses with NEON/SSE2/AVX2 SIMD kernels and emits events; the extension builds interned hash keys, strings, and containers directly on Ruby's heap during the parse, with GC-safe value stacks and epoch-evicted key caches. Generation walks Ruby objects once, streaming through fused scan-and-store escape kernels.
- Byte-exact floats. Output reproduces the json gem's fpconv (Grisu2) float format digit for digit—round-tripping is verified, not assumed.
- PGO everywhere. Local builds, CI, and every precompiled platform gem train on the benchmark corpus before the shipping compile; the precompiled binaries use portable codegen with SIMD tiers detected at runtime.
Development
bundle exec rake compile # build the extension (applies a PGO profile if present)
bundle exec rake spec # the gem-parity suite
bundle exec rake bench # PGO retrain + the parity-gated sweep vs the json gem
bundle exec rake bench:fast # the sweep without retraining
bundle exec rake "bench:ips[twitter]" # multi-gem shoot-out (benchmark-ips); no args = full corpus
License
MIT. The underlying Rust crate is MIT AND BSL-1.0 AND Apache-2.0; its
NOTICE file itemizes the derived components.