Guide
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.
Published by Tasadduq Burney
14 Jul 2025, 23:38 UTC
3 min68.6K views0

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.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.