Clojure programs rely heavily on passing around maps of data. A common approach in other libraries is to describe each entity type, combining both the keys it contains and the structure of their values. Rather than define attribute (key+value) specifications in the scope of the entity (the map), specs assign meaning to individual attributes, then collect them into maps using set semantics (on the keys). This approach allows us to start assigning (and sharing) semantics at the attribute level across our libraries and applications.

For example, most Ring middleware functions modify the request or response map with unqualified keys. However, each middleware could instead use namespaced keys with registered semantics for those keys. The keys could then be checked for conformance, creating a system with greater opportunities for collaboration and consistency.

Entity maps in spec are defined with keys :

(ns my.domain (:require [clojure.spec.alpha :as s])) (def email-regex #"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$") (s/def ::email-type (s/and string? #(re-matches email-regex %))) (s/def ::acctid int?) (s/def ::first-name string?) (s/def ::last-name string?) (s/def ::email ::email-type) (s/def ::person (s/keys :req [::first-name ::last-name ::email] :opt [::phone]))

This registers a ::person spec with the required keys ::first-name , ::last-name , and ::email , with optional key ::phone . The map spec never specifies the value spec for the attributes, only what attributes are required or optional.

When conformance is checked on a map, it does two things - checking that the required attributes are included, and checking that every registered key has a conforming value. We’ll see later where optional attributes can be useful. Also note that ALL attributes are checked via keys , not just those listed in the :req and :opt keys. Thus a bare (s/keys) is valid and will check all attributes of a map without checking which keys are required or optional.

(s/valid? ::person {::first-name "Bugs" ::last-name "Bunny" ::email "bugs@example.com"}) ;;=> true ;; Fails required key check (s/explain ::person {::first-name "Bugs"}) ;; #:my.domain{:first-name "Bugs"} - failed: (contains? % :my.domain/last-name) ;; spec: :my.domain/person ;; #:my.domain{:first-name "Bugs"} - failed: (contains? % :my.domain/email) ;; spec: :my.domain/person ;; Fails attribute conformance (s/explain ::person {::first-name "Bugs" ::last-name "Bunny" ::email "n/a"}) ;; "n/a" - failed: (re-matches email-regex %) in: [:my.domain/email] ;; at: [:my.domain/email] spec: :my.domain/email-type

Let’s take a moment to examine the explain error output on that final example:

in - the path within the data to the failing value (here, a key in the person instance)

val - the failing value, here "n/a"

spec - the spec that failed, here :my.domain/email-type

at - the path in the spec where the failing value is located

predicate - the predicate that failed, here (re-matches email-regex %)

Much existing Clojure code does not use maps with namespaced keys and so keys can also specify :req-un and :opt-un for required and optional unqualified keys. These variants specify namespaced keys used to find their specification, but the map only checks for the unqualified version of the keys.

Let’s consider a person map that uses unqualified keys but checks conformance against the namespaced specs we registered earlier:

(s/def :unq/person (s/keys :req-un [::first-name ::last-name ::email] :opt-un [::phone])) (s/conform :unq/person {:first-name "Bugs" :last-name "Bunny" :email "bugs@example.com"}) ;;=> {:first-name "Bugs", :last-name "Bunny", :email "bugs@example.com"} (s/explain :unq/person {:first-name "Bugs" :last-name "Bunny" :email "n/a"}) ;; "n/a" - failed: (re-matches email-regex %) in: [:email] at: [:email] ;; spec: :my.domain/email-type (s/explain :unq/person {:first-name "Bugs"}) ;; {:first-name "Bugs"} - failed: (contains? % :last-name) spec: :unq/person ;; {:first-name "Bugs"} - failed: (contains? % :email) spec: :unq/person

Unqualified keys can also be used to validate record attributes:

(defrecord Person [first-name last-name email phone]) (s/explain :unq/person (->Person "Bugs" nil nil nil)) ;; nil - failed: string? in: [:last-name] at: [:last-name] spec: :my.domain/last-name ;; nil - failed: string? in: [:email] at: [:email] spec: :my.domain/email-type (s/conform :unq/person (->Person "Bugs" "Bunny" "bugs@example.com" nil)) ;;=> #my.domain.Person{:first-name "Bugs", :last-name "Bunny", ;;=> :email "bugs@example.com", :phone nil}

One common occurrence in Clojure is the use of "keyword args" where keyword keys and values are passed in a sequential data structure as options. Spec provides special support for this pattern with the regex op keys* . keys* has the same syntax and semantics as keys but can be embedded inside a sequential regex structure.

(s/def ::port number?) (s/def ::host string?) (s/def ::id keyword?) (s/def ::server (s/keys* :req [::id ::host] :opt [::port])) (s/conform ::server [::id :s1 ::host "example.com" ::port 5555]) ;;=> {:my.domain/id :s1, :my.domain/host "example.com", :my.domain/port 5555}

Sometimes it will be convenient to declare entity maps in parts, either because there are different sources for requirements on an entity map or because there is a common set of keys and variant-specific parts. The s/merge spec can be used to combine multiple s/keys specs into a single spec that combines their requirements. For example consider two keys specs that define common animal attributes and some dog-specific ones. The dog entity itself can be described as a merge of those two attribute sets: