Fast JSON parser/writer for Clojure. Zero dependencies, JDK 25+.
oda is fast, low-allocation, and correct: it outperforms Jackson-backed libraries (jsonista, cheshire) on every workload we bench, read and write, allocates 2-5x less per operation, and is validated against the full JSONTestSuite corpus with bit-exact double parsing and writing (see Correctness).
Enabling the (incubating) Vector API adds SIMD string scanning for an extra boost on string-heavy documents — see Optional SIMD.
Status: alpha. API may still move.
Requires JDK 25+. The jar ships the compiled Java core; when working from a
git checkout instead, compile it once with clj -T:build javac.
(require '[s-exp.oda :as oda])
;; parse: byte-array, String or InputStream
(oda/parse "{\"a\":1,\"b\":[1.5,true,null]}")
;; => {"a" 1, "b" [1.5 true nil]}
(oda/parse "{\"a\":1}" {:key-fn keyword})
;; => {:a 1}
(oda/parse "{\"first-name\":\"Ada\"}"
{:key-fn #(keyword (clojure.string/replace % "-" "_"))})
;; => {:first_name "Ada"}
;; write
(oda/write-str {:a 1 :b [1.5 true nil]})
;; => "{\"a\":1,\"b\":[1.5,true,null]}"
(oda/write-bytes {:a 1}) ;; => byte[]
(oda/write {:a 1} output-stream) ;; progressive, bounded memory
parse:
| option | default | |
|---|---|---|
:key-fn | nil | fn of String -> key applied to object keys; nil keeps strings. clojure.core/keyword is recognized and takes an optimized interning path. Must be pure: results are cached by fn identity |
:max-depth | 1000 | maximum nesting depth (stack-overflow/DoS guard) |
write-str / write-bytes / write:
| option | default | |
|---|---|---|
:default-fn | nil | called on values of unsupported types, must return a writable value; without it unsupported types throw |
:date-format | yyyy-MM-dd'T'HH:mm:ss'Z' (UTC) | format for java.util.Date/java.time.Instant values: a pattern string or a DateTimeFormatter (zone-less formatters default to UTC) |
Maps (keys: keyword, string, symbol, number), vectors, sets, seqs,
java.util.Map/Iterable, strings, keywords, symbols, chars, UUIDs,
java.util.Date/java.time.Instant (see :date-format), all JVM numbers
(Ratio written as double), booleans, nil. NaN/Infinity throw.
JMH (forked, average time, gc-profiled), Apple M-series, JDK 25, keyword keys. All multipliers are oda's speedup relative to jsonista (Jackson); higher is better, below 1.0x jsonista is faster. SIMD columns have the Vector API enabled (see below).
| payload | read | read (SIMD) | write | write (SIMD) |
|---|---|---|---|---|
| number-heavy | 2.2x | 2.3x | 1.2x | 1.3x |
| small objects, repeated keys | 2.0x | 1.8x | 1.2x | 1.2x |
| citm_catalog | 1.5x | 1.6x | 2.6x | 2.5x |
| long ASCII strings | 1.5x | 3.6x | 1.2x | 4.8x |
| twitter.json | 1.1x | 1.2x | 2.0x | 2.0x |
| string-heavy (raw UTF-8) | 1.1x | 1.3x | 1.1x | 1.1x |
| string-heavy (\uXXXX escapes) | 0.9x | 1.0x | 1.1x | 1.1x |
Jackson's escaped-string writer is bimodal across JVM forks (~1.5ms or ~4ms per op on the string payloads); the ratios above use its fast mode.
oda also allocates 2-5x less than jsonista per operation on most payloads (e.g. citm read: 1.4MB vs 7.9MB per op). Writes allocate nothing beyond the returned array (numbers included, via a Ryū port). Reproduce with:
clj -M:jmh quick vector # or: full, scalar, plus payload/benchmark names
With the (incubating) Vector API enabled, string scanning and encoding go 16 bytes at a time.
Enable with:
clj -J--add-modules -Jjdk.incubator.vector ...
Without the module oda silently uses its scalar (SWAR) paths.
-Doda.vector=false forces scalar.
y_/n_/i_ cases)Double/parseDouble on
torture values plus thousands of generated round-tripsbyte[], fused tokenizer/builder, no token objectsPersistentArrayMap built directly for small objects, transient
PersistentHashMap above 8 keysCopyright © Max Penet. Distributed under the
Mozilla Public License 2.0
(see LICENSE).
EiselLemire.java is ported from
FastDoubleParser,
Copyright © Werner Randelshofer, MIT License. RyuDouble.java is adapted
from ryu, Copyright © Ulf Adams,
Apache License 2.0.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |