Create subschemas of malli-schemas using a spec2-inspired select notation. Works on Clojure and ClojureScript.
It's based on Rich Hickey's ideas from his talk "Maybe Not" about how spec-alpha2 might allow for schema reuse.
Follow along using deps-try:
$ deps-try io.github.eval/malli-select metosin/malli
# no install/docker
$ docker run -it --rm -v deps-try:/home/deps-try-user ghcr.io/eval/deps-try io.github.eval/malli-select metosin/malli
user=> (require '[malli-select.core :as ms]
'[malli.core :as m :refer [form] :rename {form p}])
user=> (def Person
[:map
[:name string?]
[:age pos-int?]
[:addresses [:vector [:map
[:street string?] [:zip string?]]]]])
;; require :name, everything else is optional
user=> (p (ms/select Person [:name]))
[:map
[:name string?]
[:age {:optional true} pos-int?]
[:addresses
{:optional true}
[:vector
[:map
[:street {:optional true} string?]
[:zip {:optional true} string?]]]]]
;; *if* any address is provided, it should at least have :street
user=> (p (ms/select Person [{:addresses [:street]}]))
[:map
[:name {:optional true} string?]
[:age {:optional true} pos-int?]
[:addresses
{:optional true}
[:vector
[:map [:street string?] [:zip {:optional true} string?]]]]]
;; example valid data:
;; {}, {:addresses []}, {:addresses [{:street "Main"}]}
;;
;; example invalid data:
;; {:addresses nil}, {:addresses [{}]}, {:addresses [{:street "Foo" :zip 1234}]}
;; any address provided should be a full address
user=> (p (ms/select Person [{:addresses ['*]}]))
;;
;; require all attributes of a person (shallow, i.e. address attributes become optional)
user=> (p (ms/select Person ['*]))
;; example valid data:
;; {:name "Foo" :age 18 :addresses [{}]}
;; remove any optional attribute
user=> (p (ms/select Person [{:addresses ['*]}] {:prune-optionals true}))
;; or shorter:
user=> (p (ms/select Person ^:only [{:addresses ['*]}]))
;; example valid data:
;; {:name :not-a-string}
;;
;; Typically you'd use this to generate only specific data:
user=> (require '[malli.generator :as mg])
user=> (mg/generate (ms/select Person ^:only [:name]))
{:name "sNeLdUI5KtPw"}
;; selecting something not contained in the schema:
user=> (ms/select Person [:a])
Execution error (ExceptionInfo) at malli-select.core/-fail! (core.cljc:7).
:malli-select.core/unknown-paths {:paths ([:a]), :available ([:addresses :street] [:addresses :zip] [:addresses] [:age] [:name])}
;; the unknown and available paths are also in the ex-data:
user=> (ex-data *e)
{:type :malli-select.core/unknown-paths,
:data {:paths ([:a]),
:available ([:addresses :street] [:addresses :zip] [:addresses] [:age] [:name])}}
;; bypass this check:
user=> (ms/select Person [:a] {:verify-selection false})
;; :verify-selection defaults to :throw (`:assert`, the pre-v0.8 spelling, still works);
;; :skip, nil and false disable the check.
;; other options: :log warns (stderr/console.warn) and continues...
user=> (ms/select Person [:a] {:verify-selection :log})
WARNING: :malli-select.core/unknown-paths {:paths ([:a]), :available (...)}
;; ...and a function gets the report, e.g.:
user=> (ms/select Person [:a] {:verify-selection #(log/warn "unknown paths" (:paths %))})
;; change the default via the dynamic var ms/*verify-selection*:
user=> (alter-var-root #'ms/*verify-selection* (constantly :log)) ;; CLJS: (set! ms/*verify-selection* :log)
note
Before v0.8 an invalid selection threw an AssertionError instead of an
ExceptionInfo — see the CHANGELOG if you were catching it.
See the tests for more.
Copyright (c) 2026 Gert Goet, ThinkCreate. Distributed under the MIT license. See LICENSE.
Can you improve this documentation?Edit 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 |