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)

Resources

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;
}

diagram-clojure-spec-flow.png

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.

Author: Jason Walsh

j@wal.sh

Last Updated: 2026-04-18 23:19:49

build: 2026-04-20 23:41 | sha: d110973