prompts/skills/clojure-malli/SKILL.md
Data validation with Malli schemas in Clojure. Use when working with: (0) malli, (1) data validation or coercion, (2) defining schemas for maps, collections, or domain models, (3) API request/response validation, (4) form validation, (5) runtime type checking, or when the user mentions malli, schemas, validation, data contracts, or data integrity.
npx skillsauth add ramblurr/nix-devenv clojure-malliInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Malli validates data against schemas. Schemas are Clojure data structures.
deps.edn dependency:
{:deps {metosin/malli {:mvn/version "0.20.0"}}}
See https://clojars.org/metosin/malli for the latest version.
(require '[malli.core :as m])
(require '[malli.error :as me])
;; Validate
(m/validate [:map [:name :string] [:age :int]]
{:name "Alice" :age 30})
;; => true
;; Get errors
(-> [:map [:name :string] [:age :int]]
(m/explain {:name "Alice" :age "thirty"})
(me/humanize))
;; => {:age ["should be an integer"]}
(require '[malli.transform :as mt])
;; Decode string input to proper types
(m/decode [:map [:port :int] [:active :boolean]]
{:port "8080" :active "true"}
(mt/string-transformer))
;; => {:port 8080, :active true}
;; Coerce = decode + validate (throws on error)
(m/coerce [:map [:id :int]] {:id "42"} (mt/string-transformer))
;; => {:id 42}
[:map
[:id :uuid] ;; required
[:name :string] ;; required
[:email {:optional true} :string] ;; optional
[:role {:default "user"} :string]] ;; optional with default
[:string {:min 1 :max 100}] ;; string length 1-100
[:int {:min 0 :max 150}] ;; integer range
[:enum "draft" "published"] ;; one of these values
[:re #".+@.+\..+"] ;; regex match
[:vector :int] ;; vector of ints
[:set :keyword] ;; set of keywords
[:map-of :keyword :string] ;; map with keyword keys, string values
[:tuple :double :double] ;; fixed [x, y] pair
;; Simple union
[:or :string :int]
;; Nilable
[:maybe :string] ;; string or nil
;; Tagged union with dispatch
[:multi {:dispatch :type}
[:user [:map [:type [:= :user]] [:name :string]]]
[:admin [:map [:type [:= :admin]] [:role :string]]]]
[:map
[:user [:map
[:name :string]
[:address [:map
[:city :string]
[:zip :string]]]]]]
;; BAD - creates validator every call
(defn process [data]
(when (m/validate schema data) ...))
;; GOOD - cached validator
(def valid? (m/validator schema))
(defn process [data]
(when (valid? data) ...))
;; Same for decoders
(def decode-request (m/decoder schema (mt/string-transformer)))
(def coerce-request (m/coercer schema (mt/string-transformer)))
Simplified syntax sugar for creating schemas, useful for quick map definitions.
(require '[malli.experimental.lite :as l])
(l/schema
{:map1 {:x :int
:y [:maybe :string]
:z (l/maybe :keyword)}
:map2 {:min-max [:int {:min 0 :max 10}]
:tuples (l/vector (l/tuple :int :string))
:optional (l/optional (l/maybe :boolean))
:set-of-maps (l/set {:e :int
:f :string})
:map-of-int (l/map-of :int {:s :string})}})
Produces:
[:map
[:map1
[:map
[:x :int]
[:y [:maybe :string]]
[:z [:maybe :keyword]]]]
[:map2
[:map
[:min-max [:int {:min 0, :max 10}]]
[:tuples [:vector [:tuple :int :string]]]
[:optional {:optional true} [:maybe :boolean]]
[:set-of-maps [:set [:map [:e :int] [:f :string]]]]
[:map-of-int [:map-of :int [:map [:s :string]]]]]]]
Lite functions: l/schema, l/maybe, l/optional, l/vector, l/tuple, l/set, l/map-of.
Custom registries via dynamic binding:
(binding [l/*options* {:registry (merge (m/default-schemas) {:user/id :int})}]
(l/schema {:id (l/maybe :user/id)
:child {:id :user/id}}))
Maps are open by default in malli. Do not add {:closed true} to map schemas
unless the human has explicitly confirmed it is needed for the specific use case.
Closing maps breaks extensibility and causes brittle validation failures when
upstream data adds new keys. Almost never the right default.
;; BAD - do not do this without explicit human confirmation
[:map {:closed true}
[:name :string]
[:age :int]]
;; GOOD - open maps (the default)
[:map
[:name :string]
[:age :int]]
When a key may not be present, use {:optional true} rather than [:maybe ...].
We prefer non-existence of a key over presence of a key with a nil value.
;; BAD - allows {:email nil} which is a meaningless sentinel value
[:map
[:name :string]
[:email [:maybe :string]]]
;; GOOD - key is simply absent when not provided
[:map
[:name :string]
[:email {:optional true} :string]]
Use [:maybe ...] only when nil is a meaningful, distinct value in your domain
(e.g., "user explicitly cleared this field").
coerce for safety.:closed true.{:optional true} for optional keys.tools
Use when working with Nixbot CI, forge commit statuses, Nix flake checks, nixbot.toml, Nixbot effects, or nixbot-cli - explains how Nixbot runs flake CI and how to inspect builds and logs.
testing
Use this OCP when executing or preparing to execute commands that change a live or important system, service reloads/restarts, package changes, deployments, migrations, firewall/network/access changes, credential rotation, NixOS switch/test/boot/deploy, or incident mitigation. It guides safe operations with a persisted ledger for scope, preflight, baseline, rollback, validation, and evidence.
development
Create new agent skills with proper structure, progressive disclosure, and bundled resources. Use when user wants to create, write, or build a new skill.
documentation
Naming conventions for workflow documents in prompts/. Use when creating plans, PRDs, research reports, idea capture or other workflow documents. Triggers on (1) creating new planning documents, (2) naming PRDs or research reports, (3) questions about document organization in prompts/.