Clojure Spec
Clojure Spec is a library for describing the structure of data and functions. It enables:
- Data validation
- Destructuring/parsing
- Error reporting
- Instrumentation
- Generative testing
Basic Specs
Predicate Specs
(require '[clojure.spec.alpha :as s])
(s/valid? even? 10) ; => true
(s/valid? string? "hello") ; => true
(s/def ::even-int (s/and int? even?))
(s/valid? ::even-int 4) ; => true
(s/valid? ::even-int 5) ; => false
Registry
Specs are stored in a global registry by keyword:
(s/def ::name string?)
(s/def ::age pos-int?)
(s/def ::email (s/and string? #(re-matches #".+@.+\..+" %)))
Composite Specs
Collections
(s/def ::numbers (s/coll-of number?))
(s/valid? ::numbers [1 2 3]) ; => true
(s/valid? ::numbers [1 "a"]) ; => false
(s/def ::unique-nums (s/coll-of number? :distinct true))
Maps
(s/def ::person
(s/keys :req [::name ::age]
:opt [::email]))
(s/valid? ::person
{::name "Alice" ::age 30}) ; => true
Tuples
(s/def ::point
(s/tuple double? double? double?))
(s/valid? ::point [1.0 2.0 3.0]) ; => true
Function Specs
fdef
(defn add [x y] (+ x y))
(s/fdef add
:args (s/cat :x number? :y number?)
:ret number?
:fn #(= (:ret %) (+ (-> % :args :x) (-> % :args :y))))
Instrumentation
(require '[clojure.spec.test.alpha :as st])
(st/instrument `add) ; Now add is checked
(add 1 2) ; => 3
(add "1" 2) ; Throws Exception
Generative Testing
Generating Data
(s/exercise ::person)
; => ([{:user/name "", :user/age 1} {:user/name "", :user/age 1}]
; [{:user/name "E", :user/age 1} {:user/name "E", :user/age 1}]
; ...)
Testing Functions
(st/check `add)
; => {:sym clojure.core/add,
; :spec #object[...],
; :result true}
Advanced Specs
Multi-spec
(defmulti event-type :event/type)
(defmethod event-type :login [_]
(s/keys :req [:event/type :event/user :event/time]))
(defmethod event-type :purchase [_]
(s/keys :req [:event/type :event/item :event/price :event/time]))
(s/def ::event (s/multi-spec event-type :event/type))
Regex Specs
(s/def ::config
(s/*
(s/alt :prop (s/tuple keyword? any?)
:env (s/tuple #{:env} (s/map-of keyword? string?))))
(s/conform ::config
[:db-url "localhost"
:env {:dev "DEV" :prod "PROD"}])
; => [[:prop [:db-url "localhost"]]
; [:env [:env {:dev "DEV", :prod "PROD"}]]]
Error Reporting
(s/explain ::person
{:name "Bob" :age -1})
; val: -1 fails spec: :user/age at: [:age] predicate: pos-int?
(s/explain-str ::person
{:name 100 :age 30})
; => "100 - failed: string? in: [:name] at: [:name]"
Real-world Usage
API Validation
(defn handle-request [request]
(if (s/valid? ::api-request request)
(process request)
(handle-error (s/explain-data ::api-request request))))
Database Schemas
(s/def ::user-id uuid?)
(s/def ::user
(s/keys :req [::user-id ::name ::email]
:opt [::phone ::address]))
Best Practices
- Start with specs for your core domain data
- Spec public functions first
- Use namespaced keywords
- Combine simple specs into complex ones
- Use instrumentation during development
Next Steps
Continue learning with:
- Web Development - Building web applications with Clojure