Parse JSON in Clojure: Cheshire, data.json, jsonista, and clojure.data.json
Last updated:
Clojure has three serious JSON libraries — cheshire, jsonista, and clojure.data.json — and the choice between them is mostly about tradeoffs in speed, dependencies, and ergonomics. Cheshire is the community default and wraps Jackson with a friendly API and a global encoder registry. jsonista is the speed-first option from Metosin, also Jackson-backed but with a thinner wrapper that benchmarks 2-3x faster. clojure.data.json ships with Clojure-core itself — zero external dependencies, slower, but ideal for libraries that should not impose Jackson on their consumers. This guide covers all three: idioms for parsing and generation, the keyword-key flag, custom encoders for Date and UUID values, streaming large files with parsed-seq, and the ClojureScript story where you drop down to js/JSON and js->clj instead.
Got JSON that won't parse cleanly in Clojure? Paste it into Jsonic's JSON Validator first — it pinpoints the exact line and column of trailing commas, missing quotes, and stray control characters before you hit a Cheshire parse exception.
Validate JSONClojure JSON library landscape: Cheshire, jsonista, data.json
The Clojure JSON ecosystem settled years ago around three options, and the choice between them is straightforward once you know your constraints. Cheshire is the most-downloaded library on Clojars and the one you will see in the majority of open-source Clojure projects. It wraps Jackson — the canonical Java JSON library used by Spring Boot, Jersey, and nearly every Java microservice — and exposes a clean Clojure API with a global encoder registry for custom types.
jsonista is the speed-focused alternative from Metosin (the company behind reitit and malli). It also wraps Jackson but with a thinner layer than Cheshire, exposing the Jackson ObjectMapper directly so you can tune behavior per-call rather than via a global registry. Benchmarks routinely show jsonista at 2-3x the throughput of Cheshire on encode and decode for typical Clojure data shapes — small maps, arrays of records, mixed primitives.
clojure.data.json is the Clojure-core option. It ships as part of the org.clojure/data.json contrib namespace, has no Java interop surface beyond java.io.Reader, and is implemented in pure Clojure. It is slower than the Jackson-backed libraries — by roughly 5-10x on typical payloads — but the code is small enough to audit in an afternoon and it ships zero transitive dependencies. For libraries published to Clojars, this is the polite default.
| Library | Version | Backend | Speed | Best for |
|---|---|---|---|---|
cheshire | 5.13.x | Jackson | Fast | Default for applications |
jsonista | 0.3.x | Jackson (thinner) | 2-3x Cheshire | Hot paths, high-throughput services |
clojure.data.json | 2.5.x | Pure Clojure | Slower | Libraries, zero-dep code |
Cheshire generate-string and parse-string basics
Cheshire's two workhorse functions are cheshire.core/parse-string (JSON → Clojure data) and cheshire.core/generate-string (Clojure data → JSON). Both take optional second arguments — a boolean or function for parse, an options map for generate.
;; deps.edn
{:deps {cheshire/cheshire {:mvn/version "5.13.0"}}}
;; In your namespace
(ns app.core
(:require [cheshire.core :as json]))
;; Parse a JSON string
(json/parse-string "{\"name\":\"Alice\",\"age\":30}")
;; => {"name" "Alice", "age" 30}
;; Parse with keyword keys (the most common form)
(json/parse-string "{\"name\":\"Alice\",\"age\":30}" true)
;; => {:name "Alice", :age 30}
;; Generate a JSON string (compact)
(json/generate-string {:name "Alice" :age 30})
;; => "{\"name\":\"Alice\",\"age\":30}"
;; Generate pretty-printed JSON
(json/generate-string {:name "Alice" :age 30} {:pretty true})
;; => "{\n \"name\" : \"Alice\",\n \"age\" : 30\n}"For files and streams, switch to parse-stream with a java.io.Reader — it avoids loading the entire file into a string before parsing.
(require '[clojure.java.io :as io])
(with-open [r (io/reader "data.json")]
(json/parse-stream r true))
;; Write to a file via generate-stream
(with-open [w (io/writer "out.json")]
(json/generate-stream {:name "Alice" :age 30} w {:pretty true}))parse-string and parse-stream throw com.fasterxml.jackson.core.JsonParseException on malformed input. Wrap in try/catch if you accept untrusted JSON.
Keyword vs string keys (true/false flag)
The second argument to parse-string and parse-stream controls how JSON object keys are converted. The three common forms are:
;; Default: keys stay as strings
(json/parse-string "{\"first-name\":\"Alice\"}")
;; => {"first-name" "Alice"}
;; true: convert keys to Clojure keywords
(json/parse-string "{\"first-name\":\"Alice\"}" true)
;; => {:first-name "Alice"}
;; Function: full control over key conversion
(require '[camel-snake-kebab.core :as csk])
(json/parse-string "{\"firstName\":\"Alice\"}" csk/->kebab-case-keyword)
;; => {:first-name "Alice"}When to use keyword keys: when the JSON keys are stable, known at compile time, and you want destructuring to work cleanly — (let [{:keys [name age]} user] ...) only works with keyword keys. This covers most application code talking to internal APIs.
When to keep string keys: when the JSON comes from untrusted sources (user input, third-party APIs you do not control), when keys contain characters that do not make legal keywords (spaces, leading digits, slashes), or when you accept arbitrary key shapes and intern-ing them as keywords would create unbounded memory growth. Clojure keywords are never garbage-collected — interning every distinct user ID as a keyword is a memory leak.
When to pass a function: when JSON keys come in one case convention (typically camelCase from JavaScript APIs) and you want them in another (kebab-case idiomatic Clojure). The camel-snake-kebab library is the standard tool — it ships predicates and converters for every combination of casing styles.
Custom encoders for Date, UUID, Joda types
Cheshire knows how to serialize Clojure primitives, collections, and a handful of JVM types out of the box — including java.util.Date, java.sql.Date, and java.util.UUID. For anything else (java.time.Instant, Joda DateTime, custom records), you register an encoder via cheshire.generate/add-encoder.
(require '[cheshire.generate :as gen])
;; Register an encoder for java.time.Instant
(gen/add-encoder
java.time.Instant
(fn [^java.time.Instant v ^com.fasterxml.jackson.core.JsonGenerator jg]
(.writeString jg (.toString v))))
;; Now every generate-string call uses it
(json/generate-string {:created (java.time.Instant/now)})
;; => "{\"created\":\"2026-05-23T10:15:30.123456Z\"}"
;; UUID is built-in but the default emits the raw string;
;; override to a custom shape if you need it
(gen/add-encoder
java.util.UUID
(fn [v jg]
(.writeString jg (str "urn:uuid:" v))))
;; LocalDate as ISO-8601 date only
(gen/add-encoder
java.time.LocalDate
(fn [v jg]
(.writeString jg (.toString v))))The encoder registry is global — once you call add-encoder the mapping applies process-wide. That is convenient (no per-call wrapping) but it means libraries that add-encoder on load surprise application authors. For libraries, prefer documented helper functions over global registration.
For Joda DateTime (legacy code on clj-time), use cheshire.custom or write the encoder directly: (gen/add-encoder org.joda.time.DateTime (fn [v jg] (.writeString jg (str v)))) emits ISO-8601. For new code, migrate to java.time — Joda is in maintenance mode and java.time is the JSR-310 successor.
Decode-side custom handling is harder — Cheshire decodes to plain Clojure data and does not invoke per-type constructors. If you need typed records back, run a transform pass (e.g., spec coercion, malli decode) after parse-string.
jsonista: 2-3x faster than Cheshire via Jackson direct
jsonistais Metosin's answer to "Cheshire is fine but I need it faster." It uses the same Jackson backend but exposes the ObjectMapper as a first-class value, avoiding the reflection and dispatch overhead Cheshire incurs to keep its API friendly. The result is roughly 2-3x faster encode and decode on typical payloads — the gap widens for large arrays and shrinks for tiny single-key objects.
;; deps.edn
{:deps {metosin/jsonista {:mvn/version "0.3.13"}}}
(require '[jsonista.core :as j])
;; Default mapper (string keys)
(j/read-value "{\"name\":\"Alice\"}")
;; => {"name" "Alice"}
;; Build a mapper with keyword key conversion baked in
(def mapper
(j/object-mapper {:decode-key-fn keyword}))
(j/read-value "{\"name\":\"Alice\"}" mapper)
;; => {:name "Alice"}
;; Write
(j/write-value-as-string {:name "Alice"})
;; => "{\"name\":\"Alice\"}"
;; Read directly from a file (no slurp needed)
(j/read-value (java.io.File. "data.json") mapper)
;; Read from a Reader
(with-open [r (clojure.java.io/reader "data.json")]
(j/read-value r mapper))The mapper-as-value design is what gets you the speed: you build one configured mapper once at application startup, then reuse it across every call without paying the registry-lookup overhead Cheshire pays per-call. For services with strict latency budgets (real-time bidding, game backends, financial APIs), the difference compounds at the p99.
Custom encoders in jsonista work through Jackson modules. Forjava.time support, add jackson-datatype-jsr310 and register it on the mapper:
(import '[com.fasterxml.jackson.datatype.jsr310 JavaTimeModule])
(def mapper
(j/object-mapper
{:decode-key-fn keyword
:modules [(JavaTimeModule.)]}))data.json: built-in, simpler, no dependencies
clojure.data.json lives in the org.clojure/data.json contrib namespace and is the choice when external dependencies are a problem. It is implemented in pure Clojure — no Jackson, no Java code beyond the JDK's built-in java.io.Reader. The throughput is lower than the Jackson-backed options, but the dependency footprint is exactly zero beyond Clojure itself.
;; deps.edn
{:deps {org.clojure/data.json {:mvn/version "2.5.0"}}}
(require '[clojure.data.json :as json])
;; Parse a string
(json/read-str "{\"name\":\"Alice\",\"age\":30}")
;; => {"name" "Alice", "age" 30}
;; Parse with keyword keys
(json/read-str "{\"name\":\"Alice\"}" :key-fn keyword)
;; => {:name "Alice"}
;; Write
(json/write-str {:name "Alice" :age 30})
;; => "{\"name\":\"Alice\",\"age\":30}"
;; Pretty print to *out*
(json/pprint {:name "Alice" :age 30})
;; Read from a Reader (streaming-friendly)
(with-open [r (clojure.java.io/reader "data.json")]
(json/read r :key-fn keyword))
;; Custom value transformation on the way out
(json/write-str
{:created (java.util.Date.)}
:value-fn (fn [k v]
(if (instance? java.util.Date v)
(str (.toInstant v))
v)))The API is a single namespace with five core functions (read, read-str, write, write-str, pprint) and options passed as keyword arguments rather than a registry. That makes it easier to reason about — every call is self-contained — at the cost of more code at every call site if you have many custom types.
For Clojure libraries that publish JSON support as a feature, data.json is the right pick: consumers do not pay for a Jackson dependency they may not want, and the Clojure-core stewardship guarantees long-term API stability.
Streaming JSON with parse-stream + json/parsed-seq
For files large enough that you do not want to hold the whole parsed structure in memory, there are two patterns: parse-stream for a single large document (still eager — produces the whole value, just avoids the intermediate string), and parsed-seq for newline-delimited JSON (NDJSON/JSONL) where each line is a complete record.
;; Pattern 1: single large document (still loads result into memory)
(with-open [r (clojure.java.io/reader "big.json")]
(json/parse-stream r true))
;; Pattern 2: NDJSON / JSONL — lazy sequence, bounded memory
;; events.jsonl contents:
;; {"id":1,"event":"login"}
;; {"id":2,"event":"click"}
;; {"id":3,"event":"logout"}
(with-open [r (clojure.java.io/reader "events.jsonl")]
(doseq [event (json/parsed-seq r true)]
(process-event event)))
;; parsed-seq is lazy — only one record is realized at a time,
;; so memory stays bounded regardless of file size.
;; Filtering large NDJSON files
(with-open [r (clojure.java.io/reader "events.jsonl")]
(->> (json/parsed-seq r true)
(filter #(= "login" (:event %)))
(take 100)
(into [])))JSONL is the right format for huge event streams. Each line is a self-contained JSON value, so processors can skip ahead, parallelize by line, and recover from a corrupt record without losing the whole file. If you control the producer, write JSONL instead of one giant JSON array — the consumer side becomes dramatically simpler and more memory-efficient.
For arbitrary streaming patterns (event-by-event traversal of a deeply nested single document, partial materialization), drop down to Jackson's JsonParser directly. Both Cheshire and jsonista expose it, and the token-stream API (nextToken, getCurrentName, getValueAsString) is what you need for true constant-memory parsing of giant single documents.
ClojureScript JSON: js->clj and js/JSON.parse
ClojureScript compiles to JavaScript, which has JSON.parse and JSON.stringify built in. You use them via the host-interop syntax — js/JSON — and convert between JavaScript objects and ClojureScript data structures with js->clj and clj->js. There is no Cheshire or jsonista in ClojureScript; those are JVM-only Java wrappers.
;; Parse: JSON string -> ClojureScript map with keyword keys
(defn parse-json [s]
(js->clj (.parse js/JSON s) :keywordize-keys true))
(parse-json "{\"name\":\"Alice\",\"age\":30}")
;; => {:name "Alice", :age 30}
;; Parse without keyword conversion (strings)
(js->clj (.parse js/JSON "{\"first-name\":\"Alice\"}"))
;; => {"first-name" "Alice"}
;; Stringify: ClojureScript data -> JSON string
(defn json-str [m]
(.stringify js/JSON (clj->js m)))
(json-str {:name "Alice" :age 30})
;; => "{\"name\":\"Alice\",\"age\":30}"
;; Pretty-print with JSON.stringify's third argument (indent)
(.stringify js/JSON (clj->js {:name "Alice"}) nil 2)
;; => "{\n \"name\": \"Alice\"\n}"Performance note: js->clj is recursive and allocates ClojureScript persistent collections for every level of the input. For hot paths over large JSON responses, consider keeping the result as a JS object and using interop (.-name, aget) instead of converting. The goog.object namespace from Closure has helpers for safe property access on JS objects without paying the conversion cost.
For richer typed serialization in ClojureScript— preserving keywords as keywords, BigIntegers, dates, sets, and other types JSON cannot natively express — use Cognitect's transit-cljs. Transit is a JSON-based format with type tags that round-trips Clojure values losslessly between Clojure and ClojureScript. For a typed wire format inside a Clojure/Script application, Transit beats plain JSON; for talking to external JSON APIs, the js/JSON approach above is what you need.
Key terms
- Cheshire
- The de facto Clojure JSON library — a Jackson wrapper exposing
parse-string,generate-string,parse-stream, and a global custom-encoder registry. Current line is 5.x and the API is stable. - jsonista
- Metosin's speed-focused Clojure JSON library. Also Jackson-backed but with a thinner wrapper that exposes the
ObjectMapperas a first-class value. Typically 2-3x faster than Cheshire on encode and decode. - clojure.data.json
- The Clojure-core JSON library — pure Clojure, no Java backend, zero external dependencies. Slower than the Jackson-backed options but the right choice for libraries that should not impose Jackson on consumers.
- parsed-seq
- A Cheshire function that returns a lazy sequence of parsed JSON values from a reader, one per line. The basis for streaming NDJSON/JSONL processing with bounded memory.
- keywordize-keys
- The option name (in
js->cljand elsewhere) that converts string map keys to Clojure keywords. In Cheshire and jsonista this is exposed as a positionaltrueflag or a:decode-key-fnsetting. - NDJSON / JSONL
- Newline-delimited JSON — a file format where each line is a complete JSON value. The standard format for event streams, log files, and append-only data because it supports line-by-line processing without parsing the whole file.
- Transit
- Cognitect's JSON-based format with type tags. Round-trips richer Clojure values (keywords, sets, dates, BigDecimal) losslessly between Clojure and ClojureScript. Use when the wire format is internal to a Clojure stack.
Frequently asked questions
Which Clojure JSON library should I use?
For most projects, pick Cheshire — it is the de facto community standard, wraps Jackson under the hood for solid speed, supports custom encoders for arbitrary types, and has a stable API that has barely changed across its 5.x line. Pick jsonista when you need maximum throughput; it is built by Metosin on top of Jackson with a thinner wrapper than Cheshire and benchmarks 2-3x faster on encode and decode for typical payloads. Pick clojure.data.json when you want zero external dependencies — it ships with Clojure-core distributions, has no Java interop surface, and is plenty fast for small-to-medium payloads. Avoid the older clojure.contrib.json or third-party libraries with low download counts. For a new service today: jsonista for hot paths, Cheshire for general use, data.json for libraries that should not impose dependencies on consumers.
How do I parse a JSON file in Clojure?
With Cheshire, use parse-stream with a reader rather than slurping the whole file into a string — it avoids loading the full file into memory before parsing. The pattern is: (with-open [r (clojure.java.io/reader "data.json")] (cheshire.core/parse-stream r true)) where the trailing true converts JSON object keys into Clojure keywords. For small files where simplicity matters more than memory, (cheshire.core/parse-string (slurp "data.json") true) is fine. With clojure.data.json the equivalents are (json/read r) and (json/read-str (slurp "data.json") :key-fn keyword). With jsonista you pass a java.io.File directly: (jsonista.core/read-value (jsonista.core/object-mapper {:decode-key-fn keyword}) (java.io.File. "data.json")). All three handle UTF-8 by default if the reader is UTF-8.
How do I convert JSON keys to keywords?
In Cheshire, pass true as the second argument to parse-string or parse-stream: (cheshire.core/parse-string s true). Without it, keys come back as strings — (parse-string s) returns {"name" "Alice"} while (parse-string s true) returns {:name "Alice"}. For more control, pass a function instead of true: (parse-string s csk/->kebab-case-keyword) converts snake_case JSON keys to kebab-case keywords. In clojure.data.json the equivalent is :key-fn keyword: (json/read-str s :key-fn keyword). In jsonista, configure the object mapper: (jsonista.core/object-mapper {:decode-key-fn keyword}). Keywords interop better with Clojure destructuring and core functions, but they cannot represent every legal JSON key — keys containing spaces or starting with digits become awkward keywords. For untrusted JSON with arbitrary key shapes, leaving keys as strings is safer.
How do I encode a Date object as JSON?
Cheshire supports custom encoders via cheshire.generate/add-encoder. Register one for the date type you actually use — java.util.Date, java.time.Instant, java.time.LocalDate — and provide a function that writes the value to a Jackson JsonGenerator. A common pattern: (require '[cheshire.generate :as gen]) then (gen/add-encoder java.time.Instant (fn [v jg] (.writeString jg (str v)))) which emits ISO-8601 strings. Once registered, every cheshire.core/generate-string call automatically uses it — no per-call wrapping. For Joda DateTime in legacy code, require [cheshire.custom :as custom] or write your own encoder. With clojure.data.json, use the :value-fn option: (json/write-str m :value-fn (fn [k v] (if (instance? java.util.Date v) (str (.toInstant v)) v))). jsonista takes module instances on the object mapper — add jackson-datatype-jsr310 as a dependency for full java.time support.
How do I parse JSON in ClojureScript?
ClojureScript runs on JavaScript, so you use the host JSON.parse and JSON.stringify directly — then convert between JS objects and ClojureScript data structures with js->clj and clj->js. The idiomatic decode pattern is (js->clj (.parse js/JSON s) :keywordize-keys true) which gives you a Clojure map with keyword keys. For encode, (.stringify js/JSON (clj->js m)) produces a JSON string. There is no Cheshire or jsonista in ClojureScript — those are JVM-only Java wrappers. For more sophisticated needs (transit-style typed encoding, BigDecimal preservation, custom keyword namespacing), use the transit-cljs or cognitect/transit-cljs library, which handles richer types than plain JSON. For most app code talking to JSON APIs, the js->clj / JSON.parse combination is what you want.
What's the difference between Cheshire and data.json?
Cheshire is a third-party library that wraps Jackson — the canonical Java JSON library used by Spring, Jersey, and most of the Java ecosystem. clojure.data.json is a pure-Clojure implementation that ships with Clojure-core and has no Java dependencies. Cheshire is faster on most workloads (Jackson is highly optimized), supports custom encoders via a global registry, handles streaming via parse-stream, and integrates with java.time and Joda types. clojure.data.json is slower but has zero dependencies, a smaller code surface, and is easier to audit. Cheshire's key-conversion flag is the second positional argument (true for keywords); data.json uses named options (:key-fn keyword). For a public Clojure library you publish to Clojars, prefer data.json so consumers do not transitively pull in Jackson. For application code, Cheshire is the safer default.
How do I stream a large JSON file?
For a single very large JSON document, use Cheshire's parse-stream with a buffered reader and process the result lazily where possible — Cheshire returns a fully realized map by default, so a 10GB JSON object will allocate 10GB+ of heap. For huge arrays of independent records, use the line-delimited JSON (JSONL/NDJSON) format instead: write each record on its own line, then read line by line. Cheshire supports this via parsed-seq: (with-open [r (io/reader "events.jsonl")] (doseq [event (json/parsed-seq r true)] (process event))). parsed-seq returns a lazy sequence of parsed values — one per line — so memory stays bounded. clojure.data.json has json/read-json on a PushbackReader for similar streaming. jsonista does not have native lazy support — drop down to Jackson's JsonParser API for token-level streaming of arbitrary structures.
How do I handle null vs missing in Clojure JSON?
JSON null parses to Clojure nil — they are the same value at the Clojure level. That means (:age user) returns nil whether the JSON was {"age": null} or {} (no age key at all). If the distinction matters (a PATCH endpoint where null means "set to null" but missing means "do not change"), you need to check contains? before the lookup: (contains? user :age) returns true only when the key is present. Cheshire and the other libraries do not preserve the missing/null distinction in the data structure — both collapse to nil-valued keys for null and absent keys for missing. For nullable-vs-absent semantics in Clojure, the conventional patterns are: a sentinel value like ::missing in your domain layer, optional-style records, or a wrapping map with explicit :present? keys. If you need bit-perfect round-tripping, use Transit instead — it distinguishes JSON null from undefined fields.
Related guides
- Parse JSON in Perl — JSON::XS, JSON::PP, and Cpanel::JSON::XS compared
- Parse JSON in Lua — dkjson, lua-cjson, and lunajson tradeoffs
- Parse JSON in R — jsonlite, RJSONIO, and rjson for data work
- Parse JSON in PowerShell — ConvertFrom-Json depth limits and edge cases
- JSON across languages — encode/decode patterns side-by-side
- Java JSON / Jackson — the library Cheshire and jsonista both wrap
Further reading and primary sources
- Cheshire on GitHub — Official repo, README, and changelog for the de facto Clojure JSON library
- jsonista on GitHub — Metosin's speed-focused Jackson wrapper with benchmarks against Cheshire
- clojure/data.json — Clojure-core JSON library — pure Clojure, zero dependencies
- camel-snake-kebab — Case-conversion helpers — pair with parse-string for converting JS API keys to kebab-case keywords
- Cognitect Transit — Typed JSON-based format that round-trips richer Clojure values between Clojure and ClojureScript