Skip to content
Clojure Spec: A Guided Tour

Clojure Spec: A Guided Tour

Clojure Spec (the clojure.spec.alpha library) is a system for describing the shape of data and the behavior of functions. The same spec you write to validate a value can be reused to generate example values for testing, instrument function arguments, explain failures, and express intent that stays readable.

Unlike a hand-rolled assert or a bag of keyword checks, a spec is:

  • Composable — small specs combine into big ones.
  • Reusable — validation, generation, conforming, and explanation all derive from one definition.
  • On the edge — you typically enforce it at boundaries (API input, function entry/exit), not every 10 lines inside.

This page walks from the core primitives up to generative testing, boundaries, and advanced registry organization.


What Spec Is & Why

Given a map like a request, an HTTP response, or a database row, you usually want to say what it looks like before you trust it:

;; ad-hoc, unreadable, easy to get inconsistent
(assert (and (map? req) (string? (:method req)) (pos? (:status req))))

;; spec version — a named, reusable description
(s/def ::request (s/keys :req-un [::method ::status ::path]))

The spec becomes a named contract. It documents the shape, validates it, generates examples, and explains why a value failed — all from one place.


Core Primitives

s/def + a predicate

The simplest spec is any existing clojure.core predicate:

(require '[clojure.spec.alpha :as s])

(s/def ::port int?)

(s/valid? ::port 8080)   ;=> true
(s/valid? ::port "8080") ;=> false

s/explain — why did it fail?

(s/explain ::port "8080")
;; "8080" - failed: int? in: [] spec: :user/port

s/conform — shape-matched extraction

(s/def ::with-unit (s/cat :n number? :unit #{:px :em :rem}))

(s/conform ::with-unit [10 :px])        ;=> {:n 10, :unit :px}
(s/conform ::with-unit [10 :wat])       ;=> ::s/invalid

conform returns a destructured value on success (you choose the shape with :as, :unform-able specs, or s/cat/s/keys), or ::s/invalid on failure.


Composing Specs

s/and, s/or

(s/def ::positive-number (s/and number? pos?))

(s/def ::id (s/or :uuid uuid? :slug string?))

s/keys — maps

(s/def ::name string?)
(s/def ::age pos-int?)

(s/def ::person
  (s/keys :req [::name]      ; namespaced keys must be present
          :opt [::age]))     ; optional

Namespaced keywords (::name) keep specs and data namespaced-safe — the spec registry keys on them.

s/coll-of, s/every

(s/def ::names (s/coll-of string?))
(s/def ::tags (s/coll-of keyword? :min-count 1 :distinct true))

s/alt — ordered alternation (like regex, for sequences)

(s/def ::optional-name (s/alt :no-name nil? :name string?))

clojure.spec.alpha’s regex operators (s/cat, s/alt, s/*, s/+, s/?) work on sequences, letting you describe processing pipelines and command grammars order-sensitively.


Specifying Maps & Keys

Beyond basic s/keys, you can merge specs and extend required/optional sets:

(s/def ::id int?)
(s/def ::base (s/keys :req [::id]))
(s/def ::extended (s/merge ::base (s/keys :opt [::name])))

(s/valid? ::extended {:id 1 :name "Ada"}) ;=> true
(s/valid? ::extended {:id 1})             ;=> true (name optional)
  • s/keys* — same but leaves other keys untouched in conforming/extraction.
  • Key-set specs give you clarity without rigidity: enforce what matters, ignore the rest.

Collections & Sequences

;; every element matches
(s/def ::roll (s/every pos-int?))

;; with collection modifiers
(s/def ::set-of-numbers (s/coll-of number? :into #{}))
(s/def ::sorted (s/every number? :into (sorted-set)))

The :into modifier controls what collection type the conformed/generated value lands in. s/every works on any sequable; s/coll-of is collection-aware.


Function specs with s/fdef

Function specs describe :args, :ret (return), and :fn (arg/return relationship):

(s/fdef add-vecs
  :args (s/cat :a (s/coll-of number?) :b (s/coll-of number?))
  :ret  (s/coll-of number?)
  :fn   (fn [{:keys [args ret]}] (= (count (:a args)) (count ret))))

(defn add-vecs [a b] (mapv + a b))

stest/check can now generate arbitrary argument tuples and assert every return satisfies :ret and :fn — property-based testing from a single spec.


Boundaries & Instrumentation

Validation everywhere is slow; validation at the edge is fast and catches bugs where they cross trust boundaries.

(require '[clojure.spec.test.alpha :as stest])

;; install wrapper-functions that check each call's :args/:ret
(stest/instrument `add-vecs)

(add-vecs [1 2] [3])  ; throws: :args failed ...

Custom generators with s/with-gen / s/gen

(s/def ::port (s/with-gen int?
                #(s/gen #{(range 1024 65536)})))

(s/gen ::port) ;=> a random int in [1024, 65535]

When a predicate has no good natural generator, s/with-gen supplies one — letting generation stay useful and realistic.


Generative Testing with stest/check

(stest/check `add-vecs)
;; => [{:spec #'add-vecs, :clojure.spec.test.alpha/count 5} ...]

check runs the function spec’s :args generator over many samples, exercising :ret/:fn. Example bugs it catches that unit tests often miss:

  • Argument/return arity mismatch across generated shapes.
  • Non-homogeneous collections leaking through.
  • Edge values (empty collections, empty maps, nil) that hand-picked test data skips.

Combine with targeted example cases (s/def + concrete values) to get the best of both.


The Spec Registry & Namespaced Keywords

Every s/def ::name registers under a fully-qualified keyword. This registry is your declarative schema catalog:

(s/get-spec ::person)          ;=> (s/keys :req [::name] :opt [::age])
(s/registry)                   ;=> whole map of registered specs
  • Use ::alias (auto-namespaced to current ns) to avoid collisions.
  • Group related specs under a common namespace prefix (::http/request, ::http/response).
  • Interface boundaries: describe public contracts with specs; keep internal helpers free of premature spec overhead.

Spec vs Schema vs Malli

clojure.spec.alphaSchemaMalli
Ship withClojure (bundled)Third-partyThird-party
GeneratorsBuilt-in (via s/gen)External/awkwardBuilt-in
Instrumentationstest/instrumentAdd-onAdd-on
Registry keyed onNamespaced keywordsSchemas as mapsKeywords / classes
Error messagess/explain (verbose)CustomizableVery customizable
Best forData + function contracts, generative testing, boundary validationSimple map schemas in app codeHigh-perf validation, runtime error messages, transformers

Bottom line: Spec shines when you want a single source of truth that also drives property testing and instrumentation. Reach for Malli when you need fast, customizable validation at runtime/hot paths; Schema when you want lightweight map schemas with minimal ceremony.


Pitfalls & Gotchas

  • Performance — full validation on hot inner loops is costly. Validate at boundaries (s/fdef + instrument in dev, or an API layer), not inside every function.
  • ::s/invalid vs nil — conform returning ::s/invalid is not nil; check with s/conform result equality, or use the two-arg s/conform with unform correctly.
  • Instrumentation is dev/test hygiene — stest/instrument wraps fns; remember to stest/unstrument (or run in a dev profile) so prod isn’t slowed.
  • Generators can be slow or biased — override with s/with-gen where default generation is skewed (e.g. large collections).
  • Spec is not a substitute for tests — it complements example-based tests; keep both.
  • Don’t over-spec internals — spec public contracts and the data that crosses trust boundaries.

Reference Links

  • Official docs: clojure.spec.alpha Reference and Guide
  • Clojure Guides: Spec chapters
  • Practicalli Clojure: Spec section
  • clojure.spec.test.alpha docs for stest/check, stest/instrument