What is Spec?

The spec library specifies the structure of data, validates or destructures it, and can generate data based on the spec. The clojure.spec (or cljs.spec) namespace is included in the Clojure core distribution, so no extra library is required to use it. spec is part of Clojure 1.9.

In this article, we present spec live coding examples provided by KLIPSE. (This article is a rewrite of http://clojure.org/guides/spec with live examples instead of static code snippets.)

All the coding examples below are live and editable:

  • the code is executed in your browser while you are reading
  • you can edit the code it is evaluated as you type

Setup

Let’s start by requiring the clojure.spec namespace:

(ns my.spec
  (:require [clojure.spec.alpha :as s]))

Predicates

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)

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.

(s/conform even? 999)

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)

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)
(s/valid? string? "abc")
(s/valid? #(> % 5) 10)
(s/valid? #(> % 5) 0)

Sets can also be used as predicates that match one or more literal values:

(s/valid? #{:club :diamond :heart :spade} :club) 

Registry

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 def. It’s up to you to register the specification in a namespace that makes sense (typically a namespace you control).

(s/def ::date #(instance? js/Date %))
(s/def ::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? ::date (js/Date.))
(s/conform ::suit :club)

You will see later that registered specs can (and should) be used anywhere we compose specs.

Composing predicates

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 ::big-even (s/and integer? even? #(> % 1000)))
(s/valid? ::big-even :foo)
(s/valid? ::big-even 10)
(s/valid? ::big-even 100000)

We can also use s/or to specify two alternatives:

(s/def ::name-or-id (s/or :name string?
                          :id   integer?))
(s/valid? ::name-or-id "abc") 
(s/valid? ::name-or-id 100)
(s/valid? ::name-or-id :foo) 

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, :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 ::name-or-id "abc")
(s/conform ::name-or-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)
(s/valid? (s/nilable string?) nil)

Explain

explain, explain-str and explain-data are another high-level operations in spec that can be used to report why a value does not conform to a spec. Let’s see it in action with some non-conforming examples we’ve seen so far:

(with-out-str
(s/explain ::suit 42))
(s/explain-data ::big-even 5)
(s/explain-str ::name-or-id :foo)

The explain output identifies the problematic value and the predicate it was evaluating. In the last example we see that when there are alternatives, errors across all of the alternatives will be printed.

This is just the beginning of what is possible with spec. In an upcoming article, we will present more cool features of spec:

  • Sequences
  • Entity maps
  • Multi-spec
  • Collections
  • Validations
  • Functions
  • Macros
  • Conformers

Clojurescript rocks!