Unleashing the Power of clojure.spec: Insights from Clojure/conj 2016
Table of Contents
Background
clojure.spec received significant attention at Clojure/conj 2016 including:
- Halloway's example for ETL,
- Rohner's Spectrum,
- Composing music with clojure.spec,
- Normand's drawing examples, and
- underpinings in Hickey's keynote.
Setup
Background on the rationale of clojure.spec is noted at https://clojure.org/about/spec . As noted it provices composability of entities with definitions similar to
Examples
Basic Specs
(require '[clojure.spec.alpha :as s]) ;; Define a spec for a non-empty string (s/def ::name (s/and string? #(> (count %) 0))) ;; Define a spec for an email (s/def ::email (s/and string? #(re-matches #".+@.+\..+" %))) ;; Define a spec for age (positive integer) (s/def ::age (s/and int? pos?)) ;; Composite spec for a person (s/def ::person (s/keys :req [::name ::email] :opt [::age])) ;; Validation (s/valid? ::person {::name "Alice" ::email "alice@example.com"}) ;; => true (s/explain ::person {::name "" ::email "invalid"}) ;; => fails with explanation
Generative Testing
(require '[clojure.spec.gen.alpha :as gen]) ;; Generate sample data (gen/sample (s/gen ::name) 5) ;; => ("a" "xy" "abc" "test" "hello") ;; Generate valid persons (gen/generate (s/gen ::person)) ;; => {::name "xyz" ::email "a@b.com" ::age 42}
Function Specs
(defn greet [person] (str "Hello, " (::name person) "!")) (s/fdef greet :args (s/cat :person ::person) :ret string?) ;; Instrument for development (require '[clojure.spec.test.alpha :as stest]) (stest/instrument `greet)
Spec lifecycle diagram
The diagram below traces a single registered spec through the four operations that make up the day-to-day API surface: validation, conformance/parsing, explanation, and generator-driven testing.
// clojure.spec lifecycle — registry plus four operations on one value digraph clojure_spec_flow { rankdir=LR; graph [bgcolor="white", fontname="Helvetica", fontsize=11, pad="0.3", nodesep="0.3", ranksep="0.35"]; node [shape=box, style="rounded,filled", fontname="Helvetica", fontsize=10, fillcolor="#f5f5f5", color="#888"]; edge [color="#aaa"]; // Registration phase subgraph cluster_register { label="Registration"; color="#369"; fontname="Helvetica"; fontsize=10; sdef [label="(s/def ::person ...)", color="#369"]; registry [label="spec registry\n(::person → spec)", shape=cylinder, fillcolor="#f5f5f5", color="#369"]; sdef -> registry; } // Input value input [label="input value\n{::name \"Alice\" ::email \"a@b.c\"}", color="#963"]; // Four downstream operations subgraph cluster_ops { label="Operations"; color="#693"; fontname="Helvetica"; fontsize=10; valid [label="(s/valid? ::person v)", color="#693"]; conform [label="(s/conform ::person v)", color="#d63"]; explain [label="(s/explain ::person v)", color="#d36"]; gen [label="(s/gen ::person)", color="#639"]; } // Outputs bool [label="true | false", shape=note, color="#693"]; parsed [label="parsed value\nor :clojure.spec.alpha/invalid", shape=note, color="#d63"]; report [label="human-readable\nproblem report", shape=note, color="#d36"]; generator [label="test.check generator", shape=note, color="#639"]; samples [label="generative tests\n(stest/check, gen/sample)", color="#639"]; // Wiring: registry resolves the keyword for every op registry -> valid [style=dashed]; registry -> conform [style=dashed]; registry -> explain [style=dashed]; registry -> gen [style=dashed]; // Wiring: input feeds the first three ops input -> valid; input -> conform; input -> explain; // Outputs valid -> bool; conform -> parsed; explain -> report; gen -> generator -> samples; }
Related notes
- Clojure + GraphQL Integration — Lacinia represents GraphQL schemas as EDN data; spec is the natural fit for validating resolver inputs and shaping coerced response data.
- Reagent Cookbook — front-end JWT/GraphQL stacks rely on spec (or its descendants) to validate API payloads before they cross the cljs/clj boundary.
- Reagent Development Tooling — shadow-cljs and instrumented function specs together give the REPL-driven feedback loop that makes spec practical during interactive development.
- Scheme notes — pairs and lists are the substrate spec describes; the contract-style assertions echo PLT contracts from the Scheme/Racket lineage.
Postscript (2026)
Spec never left alpha. The successor effort, clojure.alpha.spec ("Spec 2"), split spec definition from spec selection and added explicit schemas and selects, but as of 2026 it is still alpha and ships outside the core Clojure release. In practice the community has largely consolidated on Malli, which keeps spec's predicate-as-schema philosophy but treats schemas as plain data — making them serializable, transformable, and easy to derive JSON Schema from. The schema-first API trend that Swagger started has matured into OpenAPI 3.1 (now JSON-Schema-aligned), and Clojure shops typically generate OpenAPI documents from Malli registries rather than from spec. The diagram above still describes the conceptual lifecycle for both Spec 1 and Malli; what changed is that the registry is now data you can move between processes, not a JVM-resident side effect.
