Liking cljdoc? Tell your friends :D

malli-select

Clojars Project cljdoc badge Tests

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.

Quickstart

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-only Person [{: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-only Person [:name]))
{:name "sNeLdUI5KtPw"}
;; also supported: (ms/select Person [:name] {:prune-optionals true})
;; and, via metadata: (ms/select Person ^:only [:name])

;; 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.

Multi-schemas

Branches of a :multi schema are selected with a map: keys are dispatch values, values are sub-selections. The selection mirrors the shape of the schema — a vector addresses attributes of a :map, a map addresses branches of a :multi — at any nesting depth.

user=> (def Animal
         [:multi {:dispatch :type}
           [:human [:map [:type :keyword] [:name string?] [:age pos-int?]]]
           [:sized [:map [:type :keyword] [:size pos-int?]]]])

;; require :name in the :human branch; other branches become all-optional
user=> (p (ms/select Animal {:human [:name]}))
[:multi {:dispatch :type}
 [:human [:map [:type :keyword] [:name string?] [:age {:optional true} pos-int?]]]
 [:sized [:map [:type :keyword] [:size {:optional true} pos-int?]]]]

;; note that :type stays required: a keyword :dispatch key is auto-required in
;; every explicit map-branch, as data without it won't dispatch anyway.
;; (`::m/default` and nil branches are left alone - they also match data
;; *without* a dispatch value.)

;; '* addresses all branches...
user=> (p (ms/select Animal {'* [:type]}))

;; ...so the sub-selection must be satisfiable in *every* branch:
user=> (ms/select Animal {'* [:name]})
Execution error (ExceptionInfo) ...
;; :available contains what all branches have in common, e.g. [{:branch *} :type]

;; be more precise ('* merges with explicit branches):
user=> (ms/select Animal {'* [:type] :human [:name]})
;; ...or skip verification (branches lacking :name are then left as-is):
user=> (ms/select Animal {'* [:name]} {:verify-selection false})

;; a nested multi (a vector selection addresses map-attributes,
;; a map selection addresses branches):
user=> (def Person [:map [:id :int] [:pet [:multi {:dispatch :kind}
                                            [:dog [:map [:kind :keyword] [:breed string?]]]
                                            [:cat [:map [:kind :keyword] [:lives :int]]]]]])
user=> (p (ms/select Person [:id {:pet {:dog [:breed]}}]))

;; select-only drops unmentioned branches (mentioning none keeps all),
;; e.g. to generate only humans:
user=> (mg/generate (ms/select-only Animal {:human [:name]}))
{:name "x2Ep", :type :human}

Limitations:

  • Branches that are not :map schemas (e.g. [:multi ... [:str :string]]) pass through untouched and cannot be selected.
  • Numeric dispatch values 0 and 1 are unsupported (they collide with how paths are cleaned internally).
  • A function-valued :dispatch disables the dispatch-key auto-require.

LICENSE

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

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close