Learn where Clojure Spec is strongest for describing and checking data boundaries, and where a lighter validation approach may be clearer.
Clojure Spec: A library for describing the shape of data and function contracts so code can validate, conform, explain, instrument, and generate values around those descriptions.
Spec is powerful because it does more than say “valid or invalid.” It can describe data, explain failures, instrument function calls, and feed generative tests. But it is not automatically the best answer for every validation problem. Idiomatic use starts by being clear about the boundary you are trying to protect.
Spec is especially useful where data enters or leaves a subsystem:
Those boundaries benefit from explicit shape descriptions because they are where misunderstandings and malformed data cause the most damage.
Spec works best when smaller predicates and fields are named and then composed.
1(require '[clojure.spec.alpha :as s])
2
3(s/def ::name string?)
4(s/def ::age pos-int?)
5(s/def ::email (s/and string? #(re-matches #".+@.+\\..+" %)))
6
7(s/def ::user
8 (s/keys :req [::name ::age ::email]))
This approach is better than jumping straight to one large anonymous spec because the smaller pieces become reusable across validation, instrumentation, and tests.
The most basic question is whether some value conforms:
1(s/valid? ::user
2 {::name "Ava"
3 ::age 38
4 ::email "ava@example.com"})
5;; => true
But in practice, s/explain and s/explain-data are often more valuable than s/valid? because teams need to know why the value failed.
1(s/explain-data ::user
2 {::name "Ava"
3 ::age -1
4 ::email "not-an-email"})
That turns spec into a debugging and contract-communication tool, not just a predicate wrapper.
Spec can also describe function contracts with s/fdef.
1(s/fdef apply-discount
2 :args (s/cat :subtotal pos-int?
3 :discount nat-int?)
4 :ret nat-int?)
That becomes especially useful when paired with instrumentation during development or test runs. The value is not that every internal helper gets spec’d. The value is that public or risky boundaries gain executable contracts.
One reason Spec remains interesting is that its descriptions can feed generated tests. Once a function contract is described well enough, you can use clojure.spec.test.alpha/check to probe behavior beyond a few hand-picked examples.
This is strongest when:
Spec is not magical here. Weak specs produce weak generated tests. But strong specs can uncover gaps in ordinary example-based testing surprisingly well.
A common anti-pattern is blanket-specifying every internal shape because the tooling makes it possible. That usually creates:
Spec works best when it protects meaningful interfaces, not when it becomes a second shadow codebase.
Some validation problems are simpler than full Spec. For a small local check, a predicate or a tiny manual validation function may be clearer. The question is not “should I use Spec everywhere?” The better question is:
“Does this boundary benefit from a reusable, inspectable, generative description?”
If the answer is yes, Spec is a strong fit. If the answer is no, simpler validation is often more honest.
flowchart TD
A["Incoming or outgoing data"] --> B{"Is this a meaningful shared boundary?"}
B -- No --> C["Use a simpler local predicate or validation function"]
B -- Yes --> D["Model the boundary with Spec"]
D --> E["Validate, explain, instrument, and test"]
s/valid? alone without better failure reportings/explain and s/explain-data are often where the real value shows up.