clojure.spec for configuration validation

Some tools do not require configuration at all.

In March, I wrote about configuring Clojure web applications. I recommended storing the configuration in EDN files and loading them with Maailma. Something I didn’t mention at all was validating the configuration.

The problem

What happens if you mistype a configuration key? Clojure is not known for great error messages and you’ll witness this unless you validate your configuration.

Let’s say you use HTTP Kit’s HTTP server and your configuration file looks something like this:

{:http/server {:potr 3000}}

Maybe you start the server like this:

(require '[org.httpkit.server :refer [run-server]])

(defn start-server [config]
  (run-server app {:port (get-in config [:http/server :port])}))

But uh oh, there was a typo in the config! It should say :port instead of :potr. HTTP Kit is going to receive {:port nil}. Can you guess the error message?

boot.user=> (run-server app {:port nil})
java.lang.NullPointerException: 

Of course. What if you instead pass the whole configuration submap to HTTP Kit?

(defn start-server [config]
  (run-server app (get config :http/server)))

This time you won’t get any error message. The server will quietly ignore the configuration and start at the default port 8090.

I’m using HTTP Kit as an example, but this problem is not specific to it. It’s just rare in the Clojure ecosystem to give useful error messages on bad input, and Clojure’s dynamism does not help here.

This is why configuration validation matters. You’ll save yourself a lot of debugging time by using a bit of time on validation.

The clojure.spec solution

Clojure 1.9 is going to ship with clojure.spec, a library for specifying and validating data shape. My impression is that it’s primarily intended as a development and testing tool. I do not have much experience with that yet, but it works nicely for writing configuration validation code.

Let’s write a spec for the configuration above.

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

;; The top level has one required key, :http/server,
;; specified below
(s/def ::config (s/keys :req [:http/server]))

;; :http/server has one required unqualified key, :port,
;; which should be a valid port number.
(s/def :http/server (s/keys :req-un [:http/port]))
(s/def :http/port (s/int-in 0 65536))

(defn validate-config [config]
  (when-not (s/valid? ::config config)
    (s/explain ::config config)
    (throw (ex-info "Invalid configuration." (s/explain-data ::config config))))
  config)

Let’s try this with our example configuration.

boot.user=> (validate-config {:http/server {:potr 3000}})
In: [:http/server] val: {:potr 3000} fails spec: :http/server
at: [:http/server] predicate: (contains? % :port)
clojure.lang.ExceptionInfo: Invalid configuration.

Much better, and it took only a couple of lines of code!


Comments or questions? Send me an e-mail.