SkillAgentSearch skills...

Expound

Human-optimized error messages for clojure.spec

Install / Use

/learn @bhb/Expound
About this skill

Quality Score

0/100

Supported Platforms

Zed

README

Expound

Clojars Project cljdoc badge CircleCI Gitpod ready-to-code

Expound formats clojure.spec error messages in a way that is optimized for humans to read.

For example, Expound will replace a clojure.spec error message like:

val: {} fails spec: :example/place predicate: (contains? % :city)
val: {} fails spec: :example/place predicate: (contains? % :state)

with

-- Spec failed --------------------

  {}

should contain keys: :city, :state

| key    | spec    |
|========+=========|
| :city  | string? |
|--------+---------|
| :state | string? |

Comparison with clojure.spec error messages

Expound is in alpha while clojure.spec is in alpha.

Expound is supported by Clojurists Together. If you find this project useful, please consider making a monthly donation to Clojurists Together (or ask your employer to do so).

Installation

If you are using recent versions of ClojureScript, please check the compatibility guide

Leiningen/Boot

[expound "0.9.0"]

deps.edn

expound/expound {:mvn/version "0.9.0"}

Lumo

npm install @bbrinck/expound

Usage

API docs

Quick start via clj

> brew install clojure/tools/clojure
> clojure -Sdeps '{:deps {friendly/friendly {:git/url "https://gist.github.com/bhb/2686b023d074ac052dbc21f12f324f18" :sha "d532662414376900c13bed9c920181651e1efeff"}}}' -X friendly/run
user=> (require '[expound.alpha :as expound])
nil
user=> (expound/expound string? 1)
nil
-- Spec failed --------------------

  1

should satisfy

  string?

-------------------------
Detected 1 error
user=>

expound

Replace calls to clojure.spec.alpha/explain with expound.alpha/expound and to clojure.spec.alpha/explain-str with expound.alpha/expound-str.

(require '[clojure.spec.alpha :as s])
(require '[expound.alpha :as expound])

(s/def :example.place/city string?)
(s/def :example.place/state string?)
(s/def :example/place (s/keys :req-un [:example.place/city :example.place/state]))
(expound/expound :example/place {:city "Denver", :state :CO} {:print-specs? false})
;; -- Spec failed --------------------

;;   {:city ..., :state :CO}
;;                      ^^^

;; should satisfy

;;   string?

;; -------------------------
;; Detected 1 error

*explain-out*

To use other Spec functions, set clojure.spec.alpha/*explain-out* to expound/printer.

(require '[clojure.spec.alpha :as s])
(require '[expound.alpha :as expound])

(s/def :example.place/city string?)
(s/def :example.place/state string?)

;;  Use `assert`
(s/check-asserts true) ; enable asserts

;; Set var in the scope of 'binding'
(binding [s/*explain-out* expound/printer]
  (s/assert :example.place/city 1))

(set! s/*explain-out* expound/printer)
;; (or alter-var-root - see doc/faq.md)
(s/assert :example.place/city 1)

;; Use `instrument`
(require '[clojure.spec.test.alpha :as st])

(s/fdef pr-loc :args (s/cat :city :example.place/city
                            :state :example.place/state))
(defn pr-loc [city state]
  (str city ", " state))

(st/instrument `pr-loc)
(pr-loc "denver" :CO)

;; You can use `explain` without converting to expound
(s/explain :example.place/city 123)

ClojureScript considerations

Due to the way that macros are expanded in ClojureScript, you'll need to configure Expound in Clojure to use Expound during macro-expansion. This does not apply to self-hosted ClojureScript. Note the -e arg when starting ClojureScript:

clj -Srepro -Sdeps '{:deps {expound {:mvn/version "0.9.0"} org.clojure/test.check {:mvn/version "0.9.0"} org.clojure/clojurescript {:mvn/version "1.10.520"}}}' -e "(require '[expound.alpha :as expound]) (set! clojure.spec.alpha/*explain-out* expound.alpha/printer)" -m cljs.main -re node

As of this commit, ClojureScript instrumentation errors only contain data and leave formatting/printing errors to the edge of the system e.g. the REPL. To format errors in the browser, you must set up some global handler to catch errors and call repl/error->str. For instance, here is a custom formatter for Chrome devtools that uses Expound:

(require '[cljs.repl :as repl])
(require '[clojure.spec.alpha :as s])
(require '[expound.alpha :as expound])
(set! s/*explain-out* expound/printer)

(def devtools-error-formatter
  "Uses cljs.repl utilities to format ExceptionInfo objects in Chrome devtools console."
  #js{:header
      (fn [object _config]
        (when (instance? ExceptionInfo object)
          (let [message (some->> (repl/error->str object)
                                 (re-find #"[^\n]+"))]
            #js["span" message])))
      :hasBody (constantly true)
      :body (fn [object _config]
              #js["div" (repl/error->str object)])})
(defonce _
         (some-> js/window.devtoolsFormatters
                 (.unshift devtools-error-formatter)))

See this ticket for other solutions in the browser.

Printing results for check

Re-binding s/*explain-out* has no effect on the results of clojure.spec.test.alpha/summarize-results, but Expound provides the function expound/explain-results to print the results from clojure.spec.test.alpha/check.

(require '[expound.alpha :as expound]
         '[clojure.spec.test.alpha :as st]
         '[clojure.spec.alpha :as s]
         '[clojure.test.check])

(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))))
(defn ranged-rand
  "Returns random int in range start <= rand < end"
  [start end]
  (+ start (long (rand (- start end)))))

(set! s/*explain-out* expound/printer)
;; (or alter-var-root - see doc/faq.md)
(expound/explain-results (st/check `ranged-rand))
;;== Checked user/ranged-rand =================
;;
;;-- Function spec failed -----------
;;
;;  (user/ranged-rand -3 0)
;;
;;failed spec. Function arguments and return value
;;
;;  {:args {:start -3, :end 0}, :ret -5}
;;
;;should satisfy
;;
;;  (fn
;;   [%]
;;   (>= (:ret %) (-> % :args :start)))

Error messages for specs

Adding error messages

If a value fails to satisfy a predicate, Expound will print the name of the function (or <anonymous function> if the function has no name). To improve the error message, you can use expound.alpha/defmsg to add a human-readable error message to the spec.

(s/def :ex/name string?)
(expound/defmsg :ex/name "should be a string")
(expound/expound :ex/name :bob)
;; -- Spec failed --------------------
;;
;; :bob
;;
;; should be a string

You can also reuse messages by defining specs in terms of other specs:

(s/def :ex/string string?)
(expound/defmsg :ex/string "should be a string")
(s/def :ex/city :ex/string)
(expound/expound :ex/city :denver)
;; -- Spec failed --------------------
;; 
;;   :denver

;; should be a string

Built-in predicates with error messages

Expound provides a default set of type-like predicates with error messages. For example:

(expound/expound :expound.specs/pos-int -1)
;; -- Spec failed --------------------
;;
;; -1
;;
;; should be a positive integer

You can see the full list of available specs with expound.specs/public-specs.

Printer options

expound and expound-str can be configured with options:

(expound/expound :example/place {:city "Denver", :state :CO} {:print-specs? false :theme :figwheel-theme})

or, to configure the global printer:

(set! s/*explain-out* (expound/custom-printer {:show-valid-values? true :print-specs? false :theme :figwheel-theme}))
;; (or alter-var-root - see doc/faq.md)

| name | spec | default | description | |------|------|----------|-------------| | :show-valid-values? | boolean? | false | If false, replaces valid values with ... (example below) | | :value-str-fn | ifn? | provided function | Function to print bad values (example below) | | :print-specs? | boolean? | true | If true, display "Relevant specs" section. Otherwise, omit that section. | | :theme | #{:figwheel-theme :none} | :none | Enables color theme. |

:show-valid-values?

By default, printer will omit valid values and replace them with ...

(set! s/*explain-out* expound/printer)
;; (or alter-var-root - see doc/faq.md)
(s/explain :example/place {:city "Denver" :state :CO :country "USA"})

;; -- Spec failed --------------------
;;
;;   {:city ..., :state :CO, :country ...}
;;                      ^^^
;;
;; should satisfy
;;
;;   string?

You can configure Expound to show valid values:

(set! s/*explain-out* (expound/custom-printer {:show-valid-values? true}))
;; (or alter-var-root - see doc/faq.md)
(s/explain :example/place {:city "Denver" :state :CO :country "USA"})

;; -- Spec failed --------------------
;;
;; {:city "Denver", :state :CO, :country "USA"}
;;                         ^^^
;;
;; should satisfy
;;
;;   string?
:value-str-fn

You can provide your own function to display the invalid value.

;; Your implementation should meet the following spec:
(s/fdef my-value-str
        :args (s/cat
               :spec-name (s/nilable #{:args :fn :ret})
               :form any?
     

Related Skills

View on GitHub
GitHub Stars934
CategoryProduct
Updated22d ago
Forks20

Languages

Clojure

Security Score

100/100

Audited on Mar 6, 2026

No findings