clojure-spec 2025-09-10

Say you have a system with dozens of domain constructs and you want to spec them, so that you can validate them at strategic places. • Where do you put the specs? In a central spec namespace or co-located with the functions operating on the domain constructs? • How do you validate the domain constructs? Via pre-/postconditions, within the functions via s/valid?, with s/fdefs and instrumentation or with custom validators? • How do you report spec failures? Just the error or with explain or expound? • Do you validate at dev-time only or also in production? • What libs/tools do you use other than plain spec? Lot's of questions, a mini survey really.

I'll point you at https://corfield.org/blog/2019/09/13/using-spec/ for most of how we decided to do things. But TL;DR: we consider fdef to be dev/test-only; s/valid? or s/conform for production code; s/explain-data and custom heuristics for reporting; some use of exoscale/coax to perform coercion derived from Specs (trying to get away from our original use of Spec for coercion in a few places).

Alex Miller (Clojure team) 2025-09-10T18:01:57.316049Z

There are many answers to all of these of course, but general takes: • Where do you put the specs? ◦ For domain, I prefer one or more namespaces dedicated to this (consider two layers - one for types, one for attributes) and included via :as-alias ◦ For functions, in a foo.specs namespace • How do you validate the domain constructs? Via pre-/postconditions, within the functions via s/valid?, with s/fdefs and instrumentation or with custom validators? ◦ s/valid? for data validation from public inputs ◦ s/fdef / instrumentation for tests of functions • How do you report spec failures? Just the error or with explain or expound? ◦ for internal stuff, probably s/explain ◦ for public stuff, consider expound or custom • Do you validate at dev-time only or also in production? ◦ for user-side inputs, production ◦ for catching your own programming errors, dev-time only • What libs/tools do you use other than plain spec? ◦ test.chuck

Thanks for your answers, I really want to have some input on this. When I'm generating code with Claude Sonnet 4, it tends to produce co-located specs with pre-/postcondition validation. Especially pre-/postcondition validation is problematic, IMHO, because you have no influence on the error messages. Often it's not easy to see the root cause, becaue it gives you only the failed spec but not the data.

If I'm a bit settled on the options, I will make a prompt to guide AI on this.

I forgot we use test.chuck so a +1 for that and, yeah, basically agree with all of Alex's points.

Alex Miller (Clojure team) 2025-09-10T19:27:40.630139Z

I think many things I said are already in the spec guide https://clojure.org/guides/spec btw

There's nothing about code organization in that guide tho'... The guide is solid information to get a basic understanding of Spec but doesn't speak much to "best practices" in production, IMO.

👍 2

And with guides it is not always clear if they are up to date, or if best practice or tools have evolved.

👍🏻 1

Normally I start with a single namespace and I segment it with: ;;;; <construct1> ;;;; <construct2> Inside each section, I have the specs for the domain construct, a constructor that uses s/valid, a validator function, and wtv function I need to operate on it. If that grows too big I can start splitting up some sections into their own namespace.

Something like:

(ns my-app.domain)

;;;; User

(s/def :user/attribute1 ...)
(s/def :user/attribute2 ...)
...

(s/def :user/user (s/keys ...))
(s/def :user/unregistered-user (s/keys ...))
...

(defn make-user! [...]
  (let [user ...]
    (when-not (s/valid? :user/user user)
      (throw ...))
    user))

(defn valid-user? [...]
  (s/valid? :user/user user))

(defn make-unregistered-user [...]
  ...)

(defn valid-unregistered-user? [...]
  (s/valid? :user/unregistered-user user))
Sometimes I have a valid-user! that throws instead if I find that more convenient. And if I need functions to modify them I add those here too. But often you don't really need that unless you have some big invariant you need to manage. Like if adding to an attribute should subtract from another. Otherwise you can just modify it using normal functions and than call valid-user! afterwards or use the constructor to create a modified version of it.