~ / track C / clojure ecosystem
Malli schemas
IntermediateMalli is the modern Clojure schema library. Schemas are plain data, so you can transform them, generate from them, validate against them, and explain errors with them. Compared to ad-hoc validation code, a Malli schema is a single source of truth: one definition feeds runtime validation, generative testing, JSON-schema export, and documentation.
Note: examples below describe
metosin/malli. The SCI REPL on this page does not include it; run the examples in a real Clojure project.
Minimal example
A schema is data — a keyword, a vector form, or a fn. validate returns a
boolean, explain returns details:
(require '[malli.core :as m])
(m/validate :int 7)
;; => true
(m/validate [:vector :int] [1 2 3])
;; => true
(m/explain [:map [:name :string] [:age :int]]
{:name "ana" :age "old"})
;; => {:errors [{:path [:age] :in [:age] :schema :int :value "old"}]}
:map, :vector, :set, :and, :or, :enum, :tuple, :maybe, and
function/value predicates cover most needs.
Composing schemas
Schemas combine like any other data. You can def and reuse them:
(def Person
[:map
[:name :string]
[:age [:int {:min 0 :max 130}]]
[:roles [:set [:enum :admin :member :guest]]]])
(m/validate Person {:name "ana" :age 31 :roles #{:admin}})
;; => true
A schema can refer to another schema by symbol — that's how you build a library of named types.
Closed maps and :keys discipline
By default :map is open — extra keys are allowed. For APIs you usually want
the stricter :map with :closed true:
[:map {:closed true}
[:name :string]
[:age :int]]
Now {:name "ana" :age 31 :extra :stuff} fails validation, which catches
typos and stale fields at the boundary.
Coercion and decoding
Schemas double as decoders. A request body comes in as strings; Malli's transformers can coerce to the declared types, returning the typed value or an error:
(require '[malli.transform :as mt])
(m/decode [:map [:age :int]]
{:age "31"}
(mt/string-transformer))
;; => {:age 31}
You write the schema once; validation, coercion, and error messages all flow from it.
Generative testing for free
A Malli schema can produce a test.check generator. That means property
tests for any value of this type are one call away:
(require '[malli.generator :as mg])
(mg/generate Person)
;; => {:name "..." :age 47 :roles #{:guest}}
(mg/sample Person 5)
;; => five random Persons
Pair this with test.check properties (next topic) and your schemas earn
their keep.
Check yourself
? quiz
What's the practical benefit of schemas being plain Clojure data?
Exercise
Write a Malli schema Order with the fields:
:id— non-empty string:items— vector of at least one entry, each:mapwith:sku :stringand:qty :int(positive):vip?— boolean, optional
Validate the example below; then add {:closed true} and confirm an extra
top-level key now fails.
(def Order ,,, )
(m/validate Order
{:id "ord-1"
:items [{:sku "abc" :qty 2}]
:vip? true})