Using Clojure Spec for Data Validation

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 Strongest at Boundaries

Spec is especially useful where data enters or leaves a subsystem:

  • API request maps
  • event payloads
  • configuration structures
  • public function contracts
  • persisted or serialized data

Those boundaries benefit from explicit shape descriptions because they are where misunderstandings and malformed data cause the most damage.

Start by Naming Small Pieces

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.

Validation Is Only the First Step

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.

Function Specs Help at the Public Edge

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.

Generative Testing Is a Real Multiplier

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:

  • the function has clear invariants
  • inputs can vary widely
  • edge cases are easy to miss manually

Spec is not magical here. Weak specs produce weak generated tests. But strong specs can uncover gaps in ordinary example-based testing surprisingly well.

Do Not Spec Everything

A common anti-pattern is blanket-specifying every internal shape because the tooling makes it possible. That usually creates:

  • noisy specs with little reuse
  • maintenance burden
  • unclear boundaries
  • false confidence from broad but shallow annotations

Spec works best when it protects meaningful interfaces, not when it becomes a second shadow codebase.

Spec Is Not the Only Validation Tool

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"]

Common Mistakes

  • using Spec for every tiny local check
  • writing giant anonymous specs instead of composing named pieces
  • relying on s/valid? alone without better failure reporting
  • instrumenting everything indiscriminately instead of focusing on meaningful contracts
  • expecting Spec to replace ordinary domain modeling and good test design

Key Takeaways

  • Spec is strongest at important data and function boundaries.
  • Compose small named specs instead of building one giant anonymous one.
  • s/explain and s/explain-data are often where the real value shows up.
  • Function specs help most when they guard public or risky contracts.
  • Use Spec where reusable, inspectable boundary descriptions matter; use simpler validation elsewhere.

Ready to Test Your Knowledge?

Loading quiz…
Revised on Thursday, April 23, 2026