(require '[spec-tools.data-spec :as ds])
Data Specs offers an alternative, Schema-like data-driven syntax to define simple nested collection specs. Rules:
{}, Vectors [] and Sets #{}
ds/opt or ds/req for making them optional or required.ds/maybe makes it s/nilableNOTE: to avoid macros, current implementation uses the undocumented functional core of clojure.spec.alpha: every-impl, tuple-impl, map-spec-impl, nilable-impl and or-spec-impl. Support for spec-alpha2 should help to remove these.
NOTE: To use enums with data-specs, you need to wrap them: (s/spec #{:S :M :L})
(s/def ::age pos-int?)
;; a data-spec
(def person
  {::id integer?
   ::age ::age
   :boss boolean?
   (ds/req :name) string?
   (ds/opt :description) string?
   :languages #{keyword?}
   :aliases [(ds/or {:maps {:alias string?}
                     :strings string?})]
   :orders [{:id int?
             :description string?}]
   :address (ds/maybe
              {:street string?
               :zip string?})})
;; it's just data.
(def new-person
  (dissoc person ::id))
To turn a data-spec into a Spec, call ds/spec on it, providing either a options map or a qualified keyword describing the root spec name - used to generate unique names for sub-specs that will be registered. Valid options:
| Key | Description | 
|---|---|
:spec | The wrapped data-spec. | 
:name | Qualified root spec name - used to generate unique names for sub-specs. | 
:keys-spec | Function to generate the keys-specs, default ds/keys-specs. | 
:keys-default | Function to wrap not-wrapped keys, e.g. ds/opt to make keys optional by default. | 
;; options-syntax
(def person-spec
  (ds/spec
    {:name ::person
     :spec person}))
;; legacy syntax
(def person-spec
  (ds/spec ::person person))
(def new-person-spec
  (ds/spec ::person new-person))
The following specs are now registered:
(keys (st/registry #"user.*"))
; (:user/id
;  :user/age
;  :user$person/boss
;  :user$person/name
;  :user$person/description
;  :user$person/languages
;  :user$person$aliases$maps/alias
;  :user$person/orders
;  :user$person$orders/description
;  :user$person$orders/id
;  :user$person/address
;  :user$person$address/street
;  :user$person$address/zip)
And now we have specs:
(s/valid?
  new-person-spec
  {::age 63
   :boss true
   :name "Liisa"
   :languages #{:clj :cljs}
   :aliases [{:alias "Lissu"} "Liisu"]
   :orders [{:id 1, :description "cola"}
            {:id 2, :description "kebab"}]
   :description "Liisa is a valid boss"
   :address {:street "Amurinkatu 2"
             :zip "33210"}})
; true
All generated specs are wrapped into Specs Records so transformations works out of the box:
(st/encode
  new-person-spec
  {::age "63"
   :boss "true"
   :name "Liisa"
   :languages ["clj" "cljs"]
   :aliases [{:alias "Lissu"} "Liisu"]
   :orders [{:id "1", :description "cola"}
            {:id "2", :description "kebab"}]
   :description "Liisa is a valid boss"
   :address nil}
  st/string-transformer)
; {::age 63
;  :boss true
;  :name "Liisa"
;  :aliases [{:alias "Lissu"} "Liisu"]
;  :languages #{:clj :cljs}
;  :orders [{:id 1, :description "cola"}
;           {:id 2, :description "kebab"}]
;  :description "Liisa is a valid boss"
;  :address nil}
Can you improve this documentation? These fine people already did:
Craig Malone, Jordan Biserkov & Tommi ReimanEdit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs | 
| ← | Move to previous article | 
| → | Move to next article | 
| Ctrl+/ | Jump to the search field |