Validating JSON Payloads with Clojure Spec
Learn how to validate incoming JSON with Clojure spec, coerce values, and produce clear error messages when validation fails.
14 Jul 2025, 23:38 UTC

Desired outcome
Validate an incoming JSON string against a Clojure spec, coerce values to the expected types, and obtain a clear error description when the payload does not match the spec.
Necessary prerequisites
- Clojure 1.10 or later (the spec library is included in the core distribution).
org.clojure/spec.alphaversion >=0.2.173 (add viadeps.ednor Leiningen).org.clojure/data.jsonfor JSON parsing (any recent version).- A REPL or build tool to load the namespace.
- A sample JSON string to validate (e.g., a payload from an HTTP request).
Focused procedure
- Require the needed libraries in your namespace:
- Define a spec for the payload. Use namespace‑qualified keys to avoid silent mismatches.
- Parse the JSON string into a Clojure map.
- Validate and conform the parsed data against the spec.
- Handle the result: if
:valid?is false, use the explain map to produce an error message; otherwise proceed with the:conformedmap.
(ns myapp.validation
(:require [clojure.spec.alpha :as s]
[clojure.data.json :as json]))
(s/def ::id int?)
(s/def ::name string?)
(s/def ::age (s/int-in 0 150))
(s/def ::user (s/keys :req-un [::id ::name] :opt-un [::age]))
(defn parse-json [json-str]
(json/read-str json-str :key-fn keyword))
(defn validate-user [json-str]
(let [data (parse-json json-str)
result (s/conform ::user data)]
(if (= ::s/invalid result)
{:valid? false
:explain (s/explain-data ::user data)}
{:valid? true
:conformed result}))))
Expected checks
- After calling
validate-userwith a known‑good JSON string, verify that:valid?is true and that the:conformedmap contains values of the correct types (e.g.,::idis an integer). - Run
s/explain-dataon the same input; it should return an empty map ({}) indicating no unexplained problems. - Optionally perform a round‑trip test: serialize the conformed map back to JSON with
json/write-strand re‑validate; the result should again be:valid?true.
Recovery options
- If validation fails, log the explain map (
:explain) for debugging. - Return an HTTP 400 response to the client containing the explain data (or a user‑friendly rendering via
s/explain-out). - Allow the user to correct the input and resubmit.
- For non‑critical fields, you may choose to fall back to a default safe value while preserving the original payload for audit trails.
Example
Suppose you receive the following JSON payload:
{ "id": 42, "name": "Alice", "age": 30 }
Using the function above:
(validate-user "{\"id\": 42, \"name\": \"Alice\", \"age\": 30}")
;; => {:valid? true,
;; :conformed {:user/id 42, :user/name "Alice", :user/age 30}}
If the payload is malformed, e.g., missing the required ::name key:
(validate-user "{\"id\": 42}")
;; => {:valid? false,
;; :explain {
;; ::user
;; {:pred clojure.core/string?,
;; :val nil,
;; :via [::user ::name],
;; :in [:name]}}}
;; The explain map shows that the value for ::name is nil and fails the string? predicate.
Limitations and verification
- Spec validation is pure; it does not alter the original map. You must explicitly use the conformed result for further processing.
- Keys in
s/keysmust be namespace‑qualified (or resolved vias/keys*) to avoid silent mismatches. - To verify the setup, start a REPL, load the namespace containing the spec and
validate-user, then call the function with a known‑good JSON string and confirm the conformed map matches expectations. Repeat with a deliberately malformed string and ensure:valid?is false and:explainis non‑empty.