[org.clojure/clojure "1.12.0"]
The spec library (API docs) specifies the structure of data, validates or conforms it, and can generate data based on the spec.
To use spec, declare a dependency on Clojure 1.9.0 or higher:
[org.clojure/clojure "1.12.0"]
To start working with spec, require the clojure.spec.alpha
namespace at the REPL:
(require '[clojure.spec.alpha :as s])
Or include spec in your namespace:
(ns my.ns
(:require [clojure.spec.alpha :as s]))
Each spec describes a set of allowed values. There are several ways to build specs and all of them can be composed to build more sophisticated specs.
Any existing Clojure function that takes a single argument and returns a truthy value is a valid predicate spec. We can check whether a particular data value conforms to a spec using conform
:
(s/conform even? 1000)
;;=> 1000
The conform
function takes something that can be a spec and a data value. Here we are passing a predicate which is implicitly converted into a spec. The return value is "conformed". Here, the conformed value is the same as the original value - we’ll see later where that starts to deviate. If the value does not conform to the spec, the special value :clojure.spec.alpha/invalid
is returned.
If you don’t want to use the conformed value or check for :clojure.spec.alpha/invalid
, the helper valid?
can be used instead to return a boolean.
(s/valid? even? 10)
;;=> true
Note that again valid?
implicitly converts the predicate function into a spec. The spec library allows you to leverage all of the functions you already have - there is no special dictionary of predicates. Some more examples:
(s/valid? nil? nil) ;; true
(s/valid? string? "abc") ;; true
(s/valid? #(> % 5) 10) ;; true
(s/valid? #(> % 5) 0) ;; false
(import java.util.Date)
(s/valid? inst? (Date.)) ;; true
Sets can also be used as predicates that match one or more literal values:
(s/valid? #{:club :diamond :heart :spade} :club) ;; true
(s/valid? #{:club :diamond :heart :spade} 42) ;; false
(s/valid? #{42} 42) ;; true
Until now, we’ve been using specs directly. However, spec provides a central registry for globally declaring reusable specs. The registry associates a namespaced keyword with a specification. The use of namespaces ensures that we can define reusable non-conflicting specs across libraries or applications.
Specs are registered using s/def
. It’s up to you to register the specification in a namespace that makes sense (typically a namespace you control).
(s/def :order/date inst?)
(s/def :deck/suit #{:club :diamond :heart :spade})
A registered spec identifier can be used in place of a spec definition in the operations we’ve seen so far - conform
and valid?
.
(s/valid? :order/date (Date.))
;;=> true
(s/conform :deck/suit :club)
;;=> :club
You will see later that registered specs can (and should) be used anywhere we compose specs.
Spec Names
Spec names are always fully-qualified keywords. Generally, Clojure code should use keyword namespaces that are sufficiently unique such that they will not conflict with specs provided by other libraries. If you are writing a library for public use, spec namespaces should include the project name, url, or organization. Within a private organization, you may be able to use shorter names - the important thing is that they are sufficiently unique to avoid conflicts. In this guide we will often use shorter qualified names for example brevity. |
Once a spec has been added to the registry, doc
knows how to find it and print it as well:
(doc :order/date)
-------------------------
:order/date
Spec
inst?
(doc :deck/suit)
-------------------------
:deck/suit
Spec
#{:spade :heart :diamond :club}
The simplest way to compose specs is with and
and or
. Let’s create a spec that combines several predicates into a composite spec with s/and
:
(s/def :num/big-even (s/and int? even? #(> % 1000)))
(s/valid? :num/big-even :foo) ;; false
(s/valid? :num/big-even 10) ;; false
(s/valid? :num/big-even 100000) ;; true
We can also use s/or
to specify two alternatives:
(s/def :domain/name-or-id (s/or :name string?
:id int?))
(s/valid? :domain/name-or-id "abc") ;; true
(s/valid? :domain/name-or-id 100) ;; true
(s/valid? :domain/name-or-id :foo) ;; false
This or
spec is the first case we’ve seen that involves a choice during validity checking. Each choice is annotated with a tag (here, between :name
and :id
) and those tags give the branches names that can be used to understand or enrich the data returned from conform
and other spec functions.
When an or
is conformed, it returns a vector with the tag name and conformed value:
(s/conform :domain/name-or-id "abc")
;;=> [:name "abc"]
(s/conform :domain/name-or-id 100)
;;=> [:id 100]
Many predicates that check an instance’s type do not allow nil
as a valid value (string?
, number?
, keyword?
, etc). To include nil
as a valid value, use the provided function nilable
to make a spec:
(s/valid? string? nil)
;;=> false
(s/valid? (s/nilable string?) nil)
;;=> true
explain
is another high-level operation in spec that can be used to report (to *out*
) why a value does not conform to a spec. Let’s see what explain says about some non-conforming examples we’ve seen so far.
(s/explain :deck/suit 42)
;; 42 - failed: #{:spade :heart :diamond :club} spec: :deck/suit
(s/explain :num/big-even 5)
;; 5 - failed: even? spec: :num/big-even
(s/explain :domain/name-or-id :foo)
;; :foo - failed: string? at: [:name] spec: :domain/name-or-id
;; :foo - failed: int? at: [:id] spec: :domain/name-or-id
Let’s examine the output of the final example more closely. First note that there are two errors being reported - spec will evaluate all possible alternatives and report errors on every path. The parts of each error are:
val - the value in the user’s input that does not match
spec - the spec that was being evaluated
at - a path (a vector of keywords) indicating the location within the spec where the error occurred - the tags in the path correspond to any tagged part in a spec (the alternatives in an or
or alt
, the parts of a cat
, the keys in a map, etc)
predicate - the actual predicate that was not satisfied by val
in - the key path through a nested data val to the failing value. In this example, the top-level value is the one that is failing so this is essentially an empty path and is omitted.
For the first reported error we can see that the value :foo
did not satisfy the predicate string?
at the path :name
in the spec :domain/name-or-id
. The second reported error is similar but fails on the :id
path instead. The actual value is a keyword so neither is a match.
In addition to explain
, you can use explain-str
to receive the error messages as a string or explain-data
to receive the errors as data.
(s/explain-data :domain/name-or-id :foo)
;;=> #:clojure.spec.alpha{
;; :problems ({:path [:name],
;; :pred clojure.core/string?,
;; :val :foo,
;; :via [:domain/name-or-id],
;; :in []}
;; {:path [:id],
;; :pred clojure.core/int?,
;; :val :foo,
;; :via [:domain/name-or-id],
;; :in []})}
This result also demonstrates the namespace map literal syntax added in Clojure 1.9. Maps may be prefixed with |
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
:
(def email-regex #"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$")
(s/def :acct/email-type (s/and string? #(re-matches email-regex %)))
(s/def :acct/acctid int?)
(s/def :acct/first-name string?)
(s/def :acct/last-name string?)
(s/def :acct/email :acct/email-type)
(s/def :acct/person (s/keys :req [:acct/first-name :acct/last-name :acct/email]
:opt [:acct/phone]))
This registers a :acct/person
spec with the required keys :acct/first-name
, :acct/last-name
, and :acct/email
, with optional key :acct/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? :acct/person
{:acct/first-name "Bugs"
:acct/last-name "Bunny"
:acct/email "bugs@example.com"})
;;=> true
;; Fails required key check
(s/explain :acct/person
{:acct/first-name "Bugs"})
;; #:acct{:first-name "Bugs"} - failed: (contains? % :acct/last-name)
;; spec: :acct/person
;; #:acct{:first-name "Bugs"} - failed: (contains? % :acct/email)
;; spec: :acct/person
;; Fails attribute conformance
(s/explain :acct/person
{:acct/first-name "Bugs"
:acct/last-name "Bunny"
:acct/email "n/a"})
;; "n/a" - failed: (re-matches email-regex %) in: [:acct/email]
;; at: [:acct/email] spec: :acct/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 :acct/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 [:acct/first-name :acct/last-name :acct/email]
:opt-un [:acct/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: :acct/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: :acct/last-name
;; nil - failed: string? in: [:email] at: [:email] spec: :acct/email-type
(s/conform :unq/person
(->Person "Bugs" "Bunny" "bugs@example.com" nil))
;;=> #user.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 :my.config/port number?)
(s/def :my.config/host string?)
(s/def :my.config/id keyword?)
(s/def :my.config/server (s/keys* :req [:my.config/id :my.config/host]
:opt [:my.config/port]))
(s/conform :my.config/server [:my.config/id :s1
:my.config/host "example.com"
:my.config/port 5555])
;;=> #:my.config{:id :s1, :host "example.com", :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:
(s/def :animal/kind string?)
(s/def :animal/says string?)
(s/def :animal/common (s/keys :req [:animal/kind :animal/says]))
(s/def :dog/tail? boolean?)
(s/def :dog/breed string?)
(s/def :animal/dog (s/merge :animal/common
(s/keys :req [:dog/tail? :dog/breed])))
(s/valid? :animal/dog
{:animal/kind "dog"
:animal/says "woof"
:dog/tail? true
:dog/breed "retriever"})
;;=> true
One common occurrence in Clojure is to use maps as tagged entities and a special field that indicates the "type" of the map where type indicates a potentially open set of types, often with shared attributes across the types.
As previously discussed, the attributes for all types are well-specified using attributes stored in the registry by namespaced keyword. Attributes shared across entity types automatically gain shared semantics. However, we also want to be able to specify the required keys per entity type and for that spec provides multi-spec
which leverages a multimethod to provide for the specification of an open set of entity types based on a type tag.
For example, imagine an API that received event objects which shared some common fields but also had type-specific shapes. First we would register the event attributes:
(s/def :event/type keyword?)
(s/def :event/timestamp int?)
(s/def :search/url string?)
(s/def :error/message string?)
(s/def :error/code int?)
We then need a multimethod that defines a dispatch function for choosing the selector (here our :event/type
field) and returns the appropriate spec based on the value:
(defmulti event-type :event/type)
(defmethod event-type :event/search [_]
(s/keys :req [:event/type :event/timestamp :search/url]))
(defmethod event-type :event/error [_]
(s/keys :req [:event/type :event/timestamp :error/message :error/code]))
The methods should ignore their argument and return the spec for the specified type. Here we’ve fully spec’ed two possible events - a "search" event and an "error" event.
And then finally we are ready to declare our multi-spec
and try it out.
(s/def :event/event (s/multi-spec event-type :event/type))
(s/valid? :event/event
{:event/type :event/search
:event/timestamp 1463970123000
:search/url "https://clojure.org"})
;=> true
(s/valid? :event/event
{:event/type :event/error
:event/timestamp 1463970123000
:error/message "Invalid host"
:error/code 500})
;=> true
(s/explain :event/event
{:event/type :event/restart})
;; #:event{:type :event/restart} - failed: no method at: [:event/restart]
;; spec: :event/event
(s/explain :event/event
{:event/type :event/search
:search/url 200})
;; 200 - failed: string? in: [:search/url]
;; at: [:event/search :search/url] spec: :search/url
;; {:event/type :event/search, :search/url 200} - failed: (contains? % :event/timestamp)
;; at: [:event/search] spec: :event/event
Let’s take a moment to examine the explain error output on that final example. There were two different kinds of failures detected. The first failure is due to the missing required :event/timestamp
key in the event. The second is from the invalid :search/url
value (a number instead of a string). We see the same parts as prior explain errors:
in - the path within the data to the failing value. This is omitted on the first error as it’s at the root value but is the key in the map on the second error.
val - the failing value, either the full map or the individual key in the map
spec - the actual spec that failed
at - the path in the spec where the failing value occurred
predicate - the actual predicate that failed
The multi-spec
approach allows us to create an open system for spec validation, just like multimethods and protocols. New event types can be added later by just extending the event-type
multimethod.
For the special case of a homogenous collection of arbitrary size, you can use coll-of
to specify a collection of elements satisfying a predicate.
(s/conform (s/coll-of keyword?) [:a :b :c])
;;=> [:a :b :c]
(s/conform (s/coll-of number?) #{5 10 2})
;;=> #{2 5 10}
Additionally, coll-of
can be passed a number of keyword arg options:
:kind
- a predicate that the incoming collection must satisfy, such as vector?
:count
- specifies exact expected count
:min-count
, :max-count
- checks that collection has (<= min-count count max-count)
:distinct
- checks that all elements are distinct
:into
- one of [], (), {}, or #{} for output conformed value. If :into
is not specified, the input collection type will be used.
Following is an example utilizing some of these options to spec a vector containing three distinct numbers conformed as a set and some of the errors for different kinds of invalid values:
(s/def :ex/vnum3 (s/coll-of number? :kind vector? :count 3 :distinct true :into #{}))
(s/conform :ex/vnum3 [1 2 3])
;;=> #{1 2 3}
(s/explain :ex/vnum3 #{1 2 3}) ;; not a vector
;; #{1 3 2} - failed: vector? spec: :ex/vnum3
(s/explain :ex/vnum3 [1 1 1]) ;; not distinct
;; [1 1 1] - failed: distinct? spec: :ex/vnum3
(s/explain :ex/vnum3 [1 2 :a]) ;; not a number
;; :a - failed: number? in: [2] spec: :ex/vnum3
While coll-of
is good for homogenous collections of any size, another case is a fixed-size positional collection with fields of known type at different positions. For that we have tuple
.
(s/def :geom/point (s/tuple double? double? double?))
(s/conform :geom/point [1.5 2.5 -0.5])
=> [1.5 2.5 -0.5]
Note that in this case of a "point" structure with x/y/z values we actually had a choice of three possible specs:
Regular expression - (s/cat :x double? :y double? :z double?)
Allows for matching nested structure (not needed here)
Conforms to map with named keys based on the cat
tags
Collection - (s/coll-of double?)
Designed for arbitrary size homogenous collections
Conforms to a vector of the values
Tuple - (s/tuple double? double? double?)
Designed for fixed size with known positional "fields"
Conforms to a vector of the values
In this example, coll-of
will match other (invalid) values as well (like [1.0]
or [1.0 2.0 3.0 4.0])
, so it is not a suitable choice - we want fixed fields. The choice between a regular expression and tuple here is to some degree a matter of taste, possibly informed by whether you expect either the tagged return values or error output to be better with one or the other.
In addition to the support for information maps via keys
, spec also provides map-of
for maps with homogenous key and value predicates.
(s/def :game/scores (s/map-of string? int?))
(s/conform :game/scores {"Sally" 1000, "Joe" 500})
;=> {"Sally" 1000, "Joe" 500}
By default map-of
will validate but not conform keys because conformed keys might create key duplicates that would cause entries in the map to be overridden. If conformed keys are desired, pass the option :conform-keys true
.
You can also use the various count-related options on map-of
that you have with coll-of
.
Sometimes sequential data is used to encode additional structure (typically new syntax, often used in macros). spec provides the standard regular expression operators to describe the structure of a sequential data value:
Like or
, both cat
and alt
tag their "parts" - these tags are then used in the conformed value to identify what was matched, to report errors, and more.
Consider an ingredient represented by a vector containing a quantity (number) and a unit (keyword). The spec for this data uses cat
to specify the right components in the right order. Like predicates, regex operators are implicitly converted to specs when passed to functions like conform
, valid?
, etc.
(s/def :cook/ingredient (s/cat :quantity number? :unit keyword?))
(s/conform :cook/ingredient [2 :teaspoon])
;;=> {:quantity 2, :unit :teaspoon}
The data is conformed as a map with the tags as keys. We can use explain
to examine non-conforming data.
;; pass string for unit instead of keyword
(s/explain :cook/ingredient [11 "peaches"])
;; "peaches" - failed: keyword? in: [1] at: [:unit] spec: :cook/ingredient
;; leave out the unit
(s/explain :cook/ingredient [2])
;; () - failed: Insufficient input at: [:unit] spec: :cook/ingredient
Let’s now see the various occurrence operators *
, +
, and ?
:
(s/def :ex/seq-of-keywords (s/* keyword?))
(s/conform :ex/seq-of-keywords [:a :b :c])
;;=> [:a :b :c]
(s/explain :ex/seq-of-keywords [10 20])
;; 10 - failed: keyword? in: [0] spec: :ex/seq-of-keywords
(s/def :ex/odds-then-maybe-even (s/cat :odds (s/+ odd?)
:even (s/? even?)))
(s/conform :ex/odds-then-maybe-even [1 3 5 100])
;;=> {:odds [1 3 5], :even 100}
(s/conform :ex/odds-then-maybe-even [1])
;;=> {:odds [1]}
(s/explain :ex/odds-then-maybe-even [100])
;; 100 - failed: odd? in: [0] at: [:odds] spec: :ex/odds-then-maybe-even
;; opts are alternating keywords and booleans
(s/def :ex/opts (s/* (s/cat :opt keyword? :val boolean?)))
(s/conform :ex/opts [:silent? false :verbose true])
;;=> [{:opt :silent?, :val false} {:opt :verbose, :val true}]
Finally, we can use alt
to specify alternatives within the sequential data. Like cat
, alt
requires you to tag each alternative but the conformed data is a vector of tag and value.
(s/def :ex/config (s/*
(s/cat :prop string?
:val (s/alt :s string? :b boolean?))))
(s/conform :ex/config ["-server" "foo" "-verbose" true "-user" "joe"])
;;=> [{:prop "-server", :val [:s "foo"]}
;; {:prop "-verbose", :val [:b true]}
;; {:prop "-user", :val [:s "joe"]}]
If you need a description of a specification, use describe
to retrieve one. Let’s try it on some of the specifications we’ve already defined:
(s/describe :ex/seq-of-keywords)
;;=> (* keyword?)
(s/describe :ex/odds-then-maybe-even)
;;=> (cat :odds (+ odd?) :even (? even?))
(s/describe :ex/opts)
;;=> (* (cat :opt keyword? :val boolean?))
Spec also defines one additional regex operator, &
, which takes a regex operator and constrains it with one or more additional predicates. This can be used to create regular expressions with additional constraints that would otherwise require custom predicates. For example, consider wanting to match only sequences with an even number of strings:
(s/def :ex/even-strings (s/& (s/* string?) #(even? (count %))))
(s/valid? :ex/even-strings ["a"]) ;; false
(s/valid? :ex/even-strings ["a" "b"]) ;; true
(s/valid? :ex/even-strings ["a" "b" "c"]) ;; false
(s/valid? :ex/even-strings ["a" "b" "c" "d"]) ;; true
When regex ops are combined, they describe a single sequence. If you need to spec a nested sequential collection,
you must use an explicit call to spec
to start a new nested regex context. For example to describe a sequence like [:names ["a" "b"] :nums [1 2 3]]
,
you need nested regular expressions to describe the inner sequential data:
(s/def :ex/nested
(s/cat :names-kw #{:names}
:names (s/spec (s/* string?))
:nums-kw #{:nums}
:nums (s/spec (s/* number?))))
(s/conform :ex/nested [:names ["a" "b"] :nums [1 2 3]])
;;=> {:names-kw :names, :names ["a" "b"], :nums-kw :nums, :nums [1 2 3]}
If the specs were removed this spec would instead match a sequence like [:names "a" "b" :nums 1 2 3]
.
(s/def :ex/unnested
(s/cat :names-kw #{:names}
:names (s/* string?)
:nums-kw #{:nums}
:nums (s/* number?)))
(s/conform :ex/unnested [:names "a" "b" :nums 1 2 3])
;;=> {:names-kw :names, :names ["a" "b"], :nums-kw :nums, :nums [1 2 3]}
Now is a good time to step back and think about how spec can be used for runtime data validation.
One way to use spec is to explicitly call valid?
to verify input data passed to a function. You can, for example, use the existing pre- and post-condition support built into defn
:
(defn person-name
[person]
{:pre [(s/valid? :acct/person person)]
:post [(s/valid? string? %)]}
(str (:acct/first-name person) " " (:acct/last-name person)))
(person-name 42)
;; Execution error (AssertionError) at user/person-name (REPL:1).
;; Assert failed: (s/valid? :acct/person person)
(person-name {:acct/first-name "Bugs"
:acct/last-name "Bunny"
:acct/email "bugs@example.com"})
;;=> "Bugs Bunny"
When the function is invoked with something that isn’t valid :acct/person
data, the pre-condition fails. Similarly, if there was a bug in our code and the output was not a string, the post-condition would fail.
Another option is to use s/assert
within your code to assert that a value satisfies a spec. On success the value is returned and on failure an assertion error is thrown. By default assertion checking is off - this can be changed at the REPL with s/check-asserts
or on startup by setting the system property clojure.spec.check-asserts=true
.
(defn person-name
[person]
(let [p (s/assert :acct/person person)]
(str (:acct/first-name p) " " (:acct/last-name p))))
(s/check-asserts true)
(person-name 100)
;; Execution error - invalid arguments to user/person-name at (REPL:3).
;; 100 - failed: map?
A deeper level of integration is to call conform and use the return value with destructuring to pull apart the input. This will be particularly useful for complex inputs with alternate options.
Here we conform using the config specification defined above:
(defn- set-config [prop val]
;; dummy fn
(println "set" prop val))
(defn configure [input]
(let [parsed (s/conform :ex/config input)]
(if (s/invalid? parsed)
(throw (ex-info "Invalid input" (s/explain-data :ex/config input)))
(for [{prop :prop [_ val] :val} parsed]
(set-config (subs prop 1) val)))))
(configure ["-server" "foo" "-verbose" true "-user" "joe"])
Here configure calls conform
to produce data good for destructuring the config input. The result is either the special ::s/invalid
value or an annotated form of the result:
[{:prop "-server", :val [:s "foo"]}
{:prop "-verbose", :val [:b true]}
{:prop "-user", :val [:s "joe"]}]
In the success case, the parsed input is transformed into the desired shape for further processing. In the error case, we call explain-data
to generate error message data. The explain data contains information about what expression failed to conform, the path to that expression in the specification, and the predicate it was attempting to match.
The pre- and post-condition example in the previous section hinted at an interesting question - how do we define the input and output specifications for a function or macro?
Spec has explicit support for this using fdef
, which defines specifications for a function - the arguments and/or the return value spec, and optionally a function that can specify a relationship between args and return.
Let’s consider a ranged-rand
function that produces a random number in a range:
(defn ranged-rand
"Returns random int in range start <= rand < end"
[start end]
(+ start (long (rand (- end start)))))
We can then provide a specification for that function:
(s/fdef ranged-rand
:args (s/and (s/cat :start int? :end int?)
#(< (:start %) (:end %)))
:ret int?
:fn (s/and #(>= (:ret %) (-> % :args :start))
#(< (:ret %) (-> % :args :end))))
This function spec demonstrates a number of features. First the :args
is a compound spec that describes the function arguments. This spec is invoked with the args in a list, as if they were passed to (apply fn (arg-list))
. Because the args are sequential and the args are positional fields, they are almost always described using a regex op, like cat
, alt
, or *
.
The second :args
predicate takes as input the conformed result of the first predicate and verifies that start < end. The :ret
spec indicates the return is also an integer. Finally, the :fn
spec checks that the return value is >= start and < end.
Once a spec has been created for a function, the doc
for the function will also include it:
(doc ranged-rand)
-------------------------
user/ranged-rand
([start end])
Returns random int in range start <= rand < end
Spec
args: (and (cat :start int? :end int?) (< (:start %) (:end %)))
ret: int?
fn: (and (>= (:ret %) (-> % :args :start)) (< (:ret %) (-> % :args :end)))
We’ll see later how we can use a function spec for development and testing.
Higher order functions are common in Clojure and spec provides fspec
to support spec’ing them.
For example, consider the adder
function:
(defn adder [x] #(+ x %))
adder
returns a function that adds x. We can declare a function spec for adder
using fspec
for the return value:
(s/fdef adder
:args (s/cat :x number?)
:ret (s/fspec :args (s/cat :y number?)
:ret number?)
:fn #(= (-> % :args :x) ((:ret %) 0)))
The :ret
spec uses fspec
to declare that the returning function takes and returns a number. Even more interesting, the :fn
spec can state a general property that relates the :args
(where we know x) and the result we get from invoking the function returned from adder
, namely that adding 0 to it should return x.
As macros are functions that take code and produce code, they can also be spec’ed like functions. One special consideration however is that you must keep in mind that you are receiving code as data, not evaluated arguments, and that you are most commonly producing new code as data, so often it’s not helpful to spec the :ret value of a macro (as it’s just code).
For example, we could spec the clojure.core/declare
macro like this:
(s/fdef clojure.core/declare
:args (s/cat :names (s/* simple-symbol?))
:ret any?)
The Clojure macroexpander will look for and conform :args specs registered for macros at macro expansion time (not runtime!). If an error is detected, explain
will be invoked to explain the error:
(declare 100)
;; Syntax error macroexpanding clojure.core/declare at (REPL:1:1).
;; 100 - failed: simple-symbol? at: [:names]
Because macros are always checked during macro expansion, you do not need to call instrument for macro specs.
Here’s a bigger set of specs to model a game of cards:
(def suit? #{:club :diamond :heart :spade})
(def rank? (into #{:jack :queen :king :ace} (range 2 11)))
(def deck (for [suit suit? rank rank?] [rank suit]))
(s/def :game/card (s/tuple rank? suit?))
(s/def :game/hand (s/* :game/card))
(s/def :game/name string?)
(s/def :game/score int?)
(s/def :game/player (s/keys :req [:game/name :game/score :game/hand]))
(s/def :game/players (s/* :game/player))
(s/def :game/deck (s/* :game/card))
(s/def :game/game (s/keys :req [:game/players :game/deck]))
We can validate a piece of this data against the schema:
(def kenny
{:game/name "Kenny Rogers"
:game/score 100
:game/hand []})
(s/valid? :game/player kenny)
;;=> true
Or look at the errors we’ll get from some bad data:
(s/explain :game/game
{:game/deck deck
:game/players [{:game/name "Kenny Rogers"
:game/score 100
:game/hand [[2 :banana]]}]})
;; :banana - failed: suit? in: [:game/players 0 :game/hand 0 1]
;; at: [:game/players :game/hand 1] spec: :game/card
The error indicates the key path in the data structure down to the invalid value, the non-matching value, the spec part it’s trying to match, the path in that spec, and the predicate that failed.
If we have a function deal
that doles out some cards to the players we can spec that function to verify the arg and return value are both suitable data values. We can also specify a :fn
spec to verify that the count of cards in the game before the deal equals the count of cards after the deal.
(defn total-cards [{:keys [:game/deck :game/players] :as game}]
(apply + (count deck)
(map #(-> % :game/hand count) players)))
(defn deal [game] .... )
(s/fdef deal
:args (s/cat :game :game/game)
:ret :game/game
:fn #(= (total-cards (-> % :args :game))
(total-cards (-> % :ret))))
A key design constraint of spec is that all specs are also designed to act as generators of sample data that conforms to the spec (a critical requirement for property-based testing).
spec generators rely on the Clojure property testing library test.check. However, this dependency is dynamically loaded and you can use the parts of spec other than gen
, exercise
, and testing without declaring test.check as a runtime dependency. When you wish to use these parts of spec (typically during testing), you will need to declare a dev dependency on test.check.
In a deps.edn project, create a dev alias:
{...
:aliases {
:dev {:extra-deps {org.clojure/test.check {:mvn/version "0.9.0"}}}}}
In Leiningen add this to project.clj:
:profiles {:dev {:dependencies [[org.clojure/test.check "0.9.0"]]}}
In Leiningen the dev profile dependencies are included during testing but not published as a dependency or included in uber jars.
In Maven, declare your dependency as a test scope dependency:
<project>
...
<dependencies>
<dependency>
<groupId>org.clojure</groupId>
<artifactId>test.check</artifactId>
<version>0.9.0</version>
<scope>test</scope>
</dependency>
</dependency>
</project>
In your code you also need to include the clojure.spec.gen.alpha
namespace:
(require '[clojure.spec.gen.alpha :as gen])
The gen
function can be used to obtain the generator for any spec.
Once you have obtained a generator with gen
, there are several ways to use it. You can generate a single sample value with generate
or a series of samples with sample
. Let’s see some basic examples:
(gen/generate (s/gen int?))
;;=> -959
(gen/generate (s/gen nil?))
;;=> nil
(gen/sample (s/gen string?))
;;=> ("" "" "" "" "8" "W" "" "G74SmCm" "K9sL9" "82vC")
(gen/sample (s/gen #{:club :diamond :heart :spade}))
;;=> (:heart :diamond :heart :heart :heart :diamond :spade :spade :spade :club)
(gen/sample (s/gen (s/cat :k keyword? :ns (s/+ number?))))
;;=> ((:D -2.0)
;;=> (:q4/c 0.75 -1)
;;=> (:*!3/? 0)
;;=> (:+k_?.p*K.*o!d/*V -3)
;;=> (:i -1 -1 0.5 -0.5 -4)
;;=> (:?!/! 0.515625 -15 -8 0.5 0 0.75)
;;=> (:vv_z2.A??!377.+z1*gR.D9+G.l9+.t9/L34p -1.4375 -29 0.75 -1.25)
;;=> (:-.!pm8bS_+.Z2qB5cd.p.JI0?_2m.S8l.a_Xtu/+OM_34* -2.3125)
;;=> (:Ci 6.0 -30 -3 1.0)
;;=> (:s?cw*8.t+G.OS.xh_z2!.cF-b!PAQ_.E98H4_4lSo/?_m0T*7i 4.4375 -3.5 6.0 108 0.33203125 2 8 -0.517578125 -4))
What about generating a random player in our card game?
(gen/generate (s/gen :game/player))
;;=> {:game/name "sAt8r6t",
;; :game/score 233843,
;; :game/hand ([8 :spade] [5 :heart] [9 :club] [3 :heart])}
What about generating a whole game?
(gen/generate (s/gen :game/game))
;; it works! but the output is really long, so not including it here
So we can now start with a spec, extract a generator, and generate some data. All generated data will conform to the spec we used as a generator. For specs that have a conformed value different than the original value (anything using s/or, s/cat, s/alt, etc) it can be useful to see a set of generated samples plus the result of conforming that sample data.
For this we have exercise
, which returns pairs of generated and conformed values for a spec. exercise
by default produces 10 samples (like sample
) but you can pass both functions a number indicating the number of samples to produce.
(s/exercise (s/cat :k keyword? :ns (s/+ number?)) 5)
;;=>
;;([(:y -2.0) {:k :y, :ns [-2.0]}]
;; [(:_/? -1.0 0.5) {:k :_/?, :ns [-1.0 0.5]}]
;; [(:-B 0 3.0) {:k :-B, :ns [0 3.0]}]
;; [(:-!.gD*/W+ -3 3.0 3.75) {:k :-!.gD*/W+, :ns [-3 3.0 3.75]}]
;; [(:_Y*+._?q-H/-3* 0 1.25 1.5) {:k :_Y*+._?q-H/-3*, :ns [0 1.25 1.5]}])
(s/exercise (s/or :k keyword? :s string? :n number?) 5)
;;=> ([:H [:k :H]]
;; [:ka [:k :ka]]
;; [-1 [:n -1]]
;; ["" [:s ""]]
;; [-3.0 [:n -3.0]])
For spec’ed functions we also have exercise-fn
, which generates sample args, invokes the spec’ed function and returns the args and the return value.
(s/exercise-fn `ranged-rand)
=>
([(-2 -1) -2]
[(-3 3) 0]
[(0 1) 0]
[(-8 -7) -8]
[(3 13) 7]
[(-1 0) -1]
[(-69 99) -41]
[(-19 -1) -5]
[(-1 1) -1]
[(0 65) 7])
s/and
GeneratorsAll of the generators we’ve seen worked fine but there are a number of cases where they will need some additional help. One common case is when the predicate implicitly presumes values of a particular type but the spec does not specify them:
(gen/generate (s/gen even?))
;; Execution error (ExceptionInfo) at user/eval1281 (REPL:1).
;; Unable to construct gen at: [] for: clojure.core$even_QMARK_@73ab3aac
In this case spec was not able to find a generator for the even?
predicate. Most of the primitive generators in spec are mapped to the common type predicates (strings, numbers, keywords, etc).
However, spec is designed to support this case via and
- the first predicate will determine the generator and subsequent branches will act as filters by applying the predicate to the produced values (using test.check’s such-that
).
If we modify our predicate to use an and
and a predicate with a mapped generator, the even?
can be used as a filter for generated values instead:
(gen/generate (s/gen (s/and int? even?)))
;;=> -15161796
We can use many predicates to further refine the generated values. For example, say we only wanted to generate numbers that were positive multiples of 3:
(defn divisible-by [n] #(zero? (mod % n)))
(gen/sample (s/gen (s/and int?
#(> % 0)
(divisible-by 3))))
;;=> (3 9 1524 3 1836 6 3 3 927 15027)
However, it is possible to go too far with refinement and make something that fails to produce any values. The test.check such-that
that implements the refinement will throw an error if the refinement predicate cannot be resolved within a relatively small number of attempts. For example, consider trying to generate strings that happen to contain the word "hello":
;; hello, are you the one I'm looking for?
(gen/sample (s/gen (s/and string? #(clojure.string/includes? % "hello"))))
;; Error printing return value (ExceptionInfo) at clojure.test.check.generators/such-that-helper (generators.cljc:320).
;; Couldn't satisfy such-that predicate after 100 tries.
Given enough time (maybe a lot of time), the generator probably would come up with a string like this, but the underlying such-that
will make only 100 attempts to generate a value that passes the filter. This is a case where you will need to step in and provide a custom generator.
Building your own generator gives you the freedom to be either narrower and/or be more explicit about what values you want to generate. Alternately, custom generators can be used in cases where conformant values can be generated more efficiently than using a base predicate plus filtering. Spec does not trust custom generators and any values they produce will also be checked by their associated spec to guarantee they pass conformance.
There are three ways to build up custom generators - in decreasing order of preference:
Let spec create a generator based on a predicate/spec
Create your own generator from the tools in clojure.spec.gen.alpha
Use test.check or other test.check compatible libraries (like test.chuck)
The last option requires a runtime dependency on test.check so the first two options are strongly preferred over using test.check directly. |
First consider a spec with a predicate to specify keywords from a particular namespace:
(s/def :ex/kws (s/and keyword? #(= (namespace %) "my.domain")))
(s/valid? :ex/kws :my.domain/name) ;; true
(gen/sample (s/gen :ex/kws)) ;; unlikely we'll generate useful keywords this way
The simplest way to start generating values for this spec is to have spec create a generator from a fixed set of options. A set is a valid predicate spec so we can create one and ask for it’s generator:
(def kw-gen (s/gen #{:my.domain/name :my.domain/occupation :my.domain/id}))
(gen/sample kw-gen 5)
;;=> (:my.domain/occupation :my.domain/occupation :my.domain/name :my.domain/id :my.domain/name)
To redefine our spec using this custom generator, use with-gen
which takes a spec and a replacement generator:
(s/def :ex/kws (s/with-gen (s/and keyword? #(= (namespace %) "my.domain"))
#(s/gen #{:my.domain/name :my.domain/occupation :my.domain/id})))
(s/valid? :ex/kws :my.domain/name) ;; true
(gen/sample (s/gen :ex/kws))
;;=> (:my.domain/occupation :my.domain/occupation :my.domain/name ...)
Note that with-gen
(and other places that take a custom generator) take a no-arg function that returns the generator, allowing it to be lazily realized.
One downside to this approach is we are missing what property testing is really good at: automatically generating data across a wide search space to find unexpected problems.
The clojure.spec.gen.alpha namespace has a number of functions for generator "primitives" as well as "combinators" for combining them into more complicated generators.
Nearly all of the functions in the clojure.spec.gen.alpha namespace are merely wrappers that dynamically load functions of the same name in test.check. You should refer to the documentation for test.check for more details on how all of the clojure.spec.gen.alpha generator functions work. |
In this case we want our keyword to have open names but fixed namespaces. There are many ways to accomplish this but one of the simplest is to use fmap
to build up a keyword based on generated strings:
(def kw-gen-2 (gen/fmap #(keyword "my.domain" %) (gen/string-alphanumeric)))
(gen/sample kw-gen-2 5)
;;=> (:my.domain/ :my.domain/ :my.domain/1 :my.domain/1O :my.domain/l9p2)
gen/fmap
takes a function to apply and a generator. The function will be applied to each sample produced by the generator allowing us to build one generator on another.
However, we can spot a problem in the example above - generators are often designed to return "simpler" values first and any string-oriented generator will often return an empty string which is not a valid keyword. We can make a slight adjustment to omit that particular value using such-that
which lets us specify a filtering condition:
(def kw-gen-3 (gen/fmap #(keyword "my.domain" %)
(gen/such-that #(not= % "")
(gen/string-alphanumeric))))
(gen/sample kw-gen-3 5)
;;=> (:my.domain/O :my.domain/b :my.domain/ZH :my.domain/31 :my.domain/U)
Returning to our "hello" example, we now have the tools to make that generator:
(s/def :ex/hello
(s/with-gen #(clojure.string/includes? % "hello")
#(gen/fmap (fn [[s1 s2]] (str s1 "hello" s2))
(gen/tuple (gen/string-alphanumeric) (gen/string-alphanumeric)))))
(gen/sample (s/gen :ex/hello))
;;=> ("hello" "ehello3" "eShelloO1" "vhello31p" "hello" "1Xhellow" "S5bhello" "aRejhellorAJ7Yj" "3hellowPMDOgv7" "UhelloIx9E")
Here we generate a tuple of a random prefix and random suffix strings, then insert "hello" between them.
There are several cases where it’s useful to spec (and generate) values in a range and spec provides helpers for these cases.
For example, in the case of a range of integer values (for example, a bowling roll), use int-in
to spec a range (end is exclusive):
(s/def :bowling/roll (s/int-in 0 11))
(gen/sample (s/gen :bowling/roll))
;;=> (1 0 0 3 1 7 10 1 5 0)
spec also includes inst-in
for a range of instants:
(s/def :ex/the-aughts (s/inst-in #inst "2000" #inst "2010"))
(drop 50 (gen/sample (s/gen :ex/the-aughts) 55))
;;=> (#inst"2005-03-03T08:40:05.393-00:00"
;; #inst"2008-06-13T01:56:02.424-00:00"
;; #inst"2000-01-01T00:00:00.610-00:00"
;; #inst"2006-09-13T09:44:40.245-00:00"
;; #inst"2000-01-02T10:18:42.219-00:00")
Due to the generator implementation, it takes a few samples to get "interesting" so I skipped ahead a bit.
Finally, double-in
has support for double ranges and special options for checking special double values like NaN
(not a number), Infinity
, and -Infinity
.
(s/def :ex/dubs (s/double-in :min -100.0 :max 100.0 :NaN? false :infinite? false))
(s/valid? :ex/dubs 2.9)
;;=> true
(s/valid? :ex/dubs Double/POSITIVE_INFINITY)
;;=> false
(gen/sample (s/gen :ex/dubs))
;;=> (-1.0 -1.0 -1.5 1.25 -0.5 -1.0 -3.125 -1.5625 1.25 -0.390625)
spec provides a set of development and testing functionality in the clojure.spec.test.alpha
namespace, which we can include with:
(require '[clojure.spec.test.alpha :as stest])
Instrumentation validates that the :args
spec is being invoked on instrumented functions and thus provides validation for external uses of a function. Let’s turn on instrumentation for our previously spec’ed ranged-rand
function:
(stest/instrument `ranged-rand)
Instrument takes a fully-qualified symbol so we use `
here to resolve it in the context of the current namespace. If the function is invoked with args that do not conform with the :args
spec you will see an error like this:
(ranged-rand 8 5)
Execution error - invalid arguments to user/ranged-rand at (REPL:1).
{:start 8, :end 5} - failed: (< (:start %) (:end %))
The error fails in the second args predicate that checks (< start end)
. Note that the :ret
and :fn
specs are not checked with instrumentation as validating the implementation should occur at testing time.
Instrumentation can be turned off using the complementary function unstrument
. Instrumentation is likely to be useful at both development time and during testing to discover errors in calling code. It is not recommended to use instrumentation in production due to the overhead involved with checking args specs.
We mentioned earlier that clojure.spec.test.alpha
provides tools for automatically testing functions. When functions have specs, we can use check
, to automatically generate tests that check the function using the specs.
check
will generate arguments based on the :args
spec for a function, invoke the function, and check that the :ret
and :fn
specs were satisfied.
(require '[clojure.spec.test.alpha :as stest])
(stest/check `ranged-rand)
;;=> ({:spec #object[clojure.spec.alpha$fspec_impl$reify__13728 ...],
;; :clojure.spec.test.check/ret {:result true, :num-tests 1000, :seed 1466805740290},
;; :sym spec.examples.guide/ranged-rand,
;; :result true})
A keen observer will notice that |
check
also takes a number of options that can be passed to test.check to influence the test run, as well as the option to override generators for parts of the spec, by either name or path.
Imagine instead that we made an error in the ranged-rand code and swapped start and end:
(defn ranged-rand ;; BROKEN!
"Returns random int in range start <= rand < end"
[start end]
(+ start (long (rand (- start end)))))
This broken function will still create random integers, just not in the expected range. Our :fn
spec will detect the problem when checking the var:
(stest/abbrev-result (first (stest/check `ranged-rand)))
;;=> {:spec (fspec
;; :args (and (cat :start int? :end int?) (fn* [p1__3468#] (< (:start p1__3468#) (:end p1__3468#))))
;; :ret int?
;; :fn (and
;; (fn* [p1__3469#] (>= (:ret p1__3469#) (-> p1__3469# :args :start)))
;; (fn* [p1__3470#] (< (:ret p1__3470#) (-> p1__3470# :args :end))))),
;; :sym spec.examples.guide/ranged-rand,
;; :result {:clojure.spec.alpha/problems [{:path [:fn],
;; :pred (>= (:ret %) (-> % :args :start)),
;; :val {:args {:start -3, :end 0}, :ret -5},
;; :via [],
;; :in []}],
;; :clojure.spec.test.alpha/args (-3 0),
;; :clojure.spec.test.alpha/val {:args {:start -3, :end 0}, :ret -5},
;; :clojure.spec.alpha/failure :test-failed}}
check
has reported an error in the :fn
spec. We can see the arguments passed were -3 and 0 and the return value was -5, which is out of the expected range.
To test all of the spec’ed functions in a namespace (or multiple namespaces), use enumerate-namespace
to generate the set of symbols naming vars in the namespace:
(-> (stest/enumerate-namespace 'user) stest/check)
And you can check all of the spec’ed functions by calling stest/check
without any arguments.
check
and instrument
While both instrument
(for enabling :args
checking) and check
(for generating tests of a function) are useful tools, they can be combined to provide even deeper levels of test coverage.
instrument
takes a number of options for changing the behavior of instrumented functions, including support for swapping in alternate (narrower) specs, stubbing functions (by using the :ret
spec to generate results), or replacing functions with an alternate implementation.
Consider the case where we have a low-level function that invokes a remote service and a higher-level function that calls it.
;; code under test
(defn invoke-service [service request]
;; invokes remote service
)
(defn run-query [service query]
(let [{:svc/keys [result error]} (invoke-service service {:svc/query query})]
(or result error)))
We can spec these functions using the following specs:
(s/def :svc/query string?)
(s/def :svc/request (s/keys :req [:svc/query]))
(s/def :svc/result (s/coll-of string? :gen-max 3))
(s/def :svc/error int?)
(s/def :svc/response (s/or :ok (s/keys :req [:svc/result])
:err (s/keys :req [:svc/error])))
(s/fdef invoke-service
:args (s/cat :service any? :request :svc/request)
:ret :svc/response)
(s/fdef run-query
:args (s/cat :service any? :query string?)
:ret (s/or :ok :svc/result :err :svc/error))
And then we want to test the behavior of run-query
while stubbing out invoke-service
with instrument
so that the remote service is not invoked:
(stest/instrument `invoke-service {:stub #{`invoke-service}})
;;=> [user/invoke-service]
(invoke-service nil {:svc/query "test"})
;;=> #:svc{:error -11}
(invoke-service nil {:svc/query "test"})
;;=> #:svc{:result ["kq0H4yv08pLl4QkVH8" "in6gH64gI0ARefv3k9Z5Fi23720gc"]}
(stest/summarize-results (stest/check `run-query)) ;; might take a bit
;;=> {:total 1, :check-passed 1}
The first call here instruments and stubs invoke-service
. The second and third calls demonstrate that calls to invoke-service
now return generated results (rather than hitting a service). Finally, we can use check
on the higher level function to test that it behaves properly based on the generated stub results returned from invoke-service
.
In this guide we have covered most of the features for designing and using specs and generators. We expect to add some more advanced generator techniques and help on testing in a future update.
Original author: Alex Miller