CodeToLive

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: