Liking cljdoc? Tell your friends :D

oda

Clojars Project

Fast JSON parser/writer for Clojure. Zero dependencies, JDK 25+.

oda works directly on UTF-8 bytes and builds Clojure persistent data structures without intermediate representations. It is faster than Jackson-backed libraries (jsonista, cheshire) across typical workloads, on both read and write, with optional SIMD string scanning via the Vector API (--add-modules jdk.incubator.vector) for an extra boost on string-heavy documents — see Optional SIMD.

Status: alpha. API may still move.

Installation

;; deps.edn
com.s-exp/oda {:mvn/version "1.0.0-alphaN"}

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.

Usage

(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

Options

parse:

optiondefault
:key-fnnilfn 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-depth1000maximum nesting depth (stack-overflow/DoS guard)

write-str / write-bytes / write:

optiondefault
:default-fnnilcalled on values of unsupported types, must return a writable value; without it unsupported types throw

Supported types (write)

Maps (keys: keyword, string, symbol, number), vectors, sets, seqs, java.util.Map/Iterable, strings, keywords, symbols, chars, UUIDs, all JVM numbers (Ratio written as double), booleans, nil. NaN/Infinity throw.

Performance

Criterium means, Apple M-series, JDK 25, vs jsonista (Jackson). Read, keyword keys:

payloadodajsonistaspeedup
number-heavy144µs349µs2.4x
citm_catalog1889µs3066µs1.6x
small objects, repeated keys374µs562µs1.5x
string-heavy (raw UTF-8)1106µs1292µs1.2x
string-heavy (\uXXXX escapes)1480µs1616µs1.1x
twitter.json1275µs1388µs1.1x

Write is 1.3-1.5x faster than jsonista on the same payloads except long-unicode-string documents (~0.9x). Run clj -M:bench -m s-exp.oda.bench to reproduce.

Optional SIMD

With the (incubating) Vector API enabled, long-string payloads improve further (twitter +10%, raw string-heavy +21%):

clj -J--add-modules -Jjdk.incubator.vector ...

Without the module oda silently uses its scalar (SWAR) paths. -Doda.vector=false forces scalar.

Correctness

  • full JSONTestSuite corpus
  • differential testing against jsonista (corpus + generative)
  • doubles are correctly rounded (Eisel-Lemire, validated bit-exact against Double/parseDouble on torture values and generative corpora)
  • duplicate object keys: last wins

Design notes

  • single-pass parser over byte[], fused tokenizer/builder, no token objects
  • SWAR (8-byte) string scanning, optional 16-byte SIMD via Vector API
  • object keys canonicalized through fixed-size lossy caches (no locks, no thread-locals, bounded memory, virtual-thread friendly)
  • PersistentArrayMap built directly for small objects, transient PersistentHashMap above 8 keys
  • numbers: inline long accumulation; doubles via a fused Eisel-Lemire conversion (ported from FastDoubleParser, MIT)
  • writer: flat instanceof dispatch, pre-escaped key fragment caches, pair-table long rendering; streaming writes flush a 64KB buffer

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close