Malli
High-performance data-driven data specification library for Clojure/Script.
Install / Use
/learn @metosin/MalliREADME
malli
Data-driven Schemas for Clojure/Script and babashka.
Metosin Open Source Status: Active. Stability: well matured alpha.
<img src="docs/img/malli.png" width=130 align="right"/>- Schema definitions as data
- Vector, Map and Lite syntaxes
- Validation and Value Transformation
- First class Error Messages with Spell Checking
- Generating values from Schemas
- Inferring Schemas from sample values and Destructuring.
- Tools for Programming with Schemas
- Parsing and Unparsing values
- Enumeration, Sequence, Vector, and Set Schemas
- Persisting schemas, even function schemas
- Immutable, Mutable, Dynamic, Lazy and Local Schema Registries
- Schema Transformations to JSON Schema, Swagger2, and descriptions in english
- Multi-schemas, Recursive Schemas and Default values
- Function Schemas with dynamic and static schema checking
- Integrates with both clj-kondo and Typed Clojure
- Visualizing Schemas with DOT and PlantUML
- Pretty development time errors
- Fast
Presentations:
- Transforming Data With Malli and Meander
- High-Performance Schemas in Clojure/Script with Malli 1/2
- ClojureStream Podcast: Malli with Tommi Reiman
- Structure and Interpretation of Malli Regex Schemas
- LNDCLJ 9.12.2020: Designing with Malli, slides here
- Malli, Data-Driven Schemas for Clojure/Script
- CEST 2.6.2020: Data-driven Rapid Application Development with Malli
- ClojureD 2020: Malli: Inside Data-driven Schemas, slides here
Try the online demo, see also some 3rd Party Libraries.
Want to contribute? See the Development guide.
<img src="docs/img/malli-defn.png" width="600" />Hi! We are Metosin, a consulting company. These libraries have evolved out of the work we do for our clients. We maintain & develop this project, for you, for free. Issues and pull requests welcome! However, if you want more help using the libraries, or want us to build something as cool for you, consider our commercial support.
Motivation
We are building dynamic multi-tenant systems where data models should be first-class: they should drive the runtime value transformations, forms and processes. We should be able to edit the models at runtime, persist them and load them back from a database and over the wire, for both Clojure and ClojureScript. Think of JSON Schema, but for Clojure/Script.
Hasn't the problem been solved (many times) already?
There is Schema, which is an awesome, proven and collaborative open-source project, and we absolutely love it. We still use it in many of our projects. The sad part: serializing & de-serializing schemas is non-trivial and there is no proper support on branching.
Spec is the de facto data specification library for Clojure. It has many great ideas, but it is opinionated with macros, global registry, and it doesn't have any support for runtime transformations. Spec-tools was created to "fix" some of the things, but after five years of developing it, it's still a kind of hack and not fun to maintain.
So, we decided to spin out our own library, which would do all the things we feel is important for dynamic system development. It's based on the best parts of the existing libraries and several project-specific tools we have done over the years.
If you have expectations (of others) that aren't being met, those expectations are your own responsibility. You are responsible for your own needs. If you want things, make them.
- Rich Hickey, Open Source is Not About You
The library
Malli requires Clojure 1.11 or ClojureScript 1.11.51.
Malli is tested with the LTS releases Java 8, 11, 17, 21 and 25.
Quickstart
(require '[malli.core :as m])
(def UserId :string)
(def Address
[:map
[:street :string]
[:country [:enum "FI" "UA"]]])
(def User
[:map
[:id #'UserId]
[:address #'Address]
[:friends [:set {:gen/max 2} [:ref #'User]]]])
(require '[malli.generator :as mg])
(mg/generate User)
;{:id "AC",
; :address {:street "mf", :country "UA"},
; :friends #{{:id "1dm",
; :address {:street "8", :country "UA"},
; :friends #{}}}}
(m/validate User *1)
; => true
Syntax
Malli supports Vector, Map and Lite syntaxes.
Vector syntax
The default syntax uses vectors, inspired by hiccup:
<!-- :test-doc-blocks/skip -->type
[type & children]
[type properties & children]
Examples:
;; just a type (String)
:string
;; type with properties
[:string {:min 1, :max 10}]
;; type with properties and children
[:tuple {:title "location"} :double :double]
;; a function schema of :int -> :int
[:=> [:cat :int] :int]
[:-> :int :int]
Usage:
(require '[malli.core :as m])
(def non-empty-string
(m/schema [:string {:min 1}]))
(m/schema? non-empty-string)
;; => true
(m/validate non-empty-string "")
;; => false
(m/validate non-empty-string "kikka")
;; => true
(m/form non-empty-string)
;; => [:string {:min 1}]
Map syntax
Alternative map-syntax, similar to cljfx:
NOTE: For now, Map syntax in considered as internal, so don't use it as a database persistency model.
;; just a type (String)
{:type :string}
;; type with properties
{:type :string
:properties {:min 1, :max 10}}
;; type with properties and children
{:type :tuple
:properties {:title "location"}
:children [{:type :double}
{:type :double}]}
;; a function schema of :int -> :int
{:type :=>
:input {:type :cat, :children [{:type :int}]}
:output :int}
{:type :->
:children [{:type :int} {:type :int}]}
Usage:
(def non-empty-string
(m/from-ast {:type :string
:properties {:min 1}}))
(m/schema? non-empty-string)
;; => true
(m/validate non-empty-string "")
;; => false
(m/validate non-empty-string "kikka")
;; => true
(m/ast non-empty-string)
;; => {:type :string
;; :properties {:min 1}}
Map-syntax is also called the Schema AST.
Why multiple syntaxes?
Malli started with just the Vector syntax. It's really powerful and relatively easy to read, but not optimal for all use cases.
We introduced Map Syntax as we found out that the overhead of parsing large amount of vector-syntaxes can be a deal-breaker when running on slow single-threaded environments like Javascript on mobile phones. Map-syntax allows lazy and parseless Schema Creation.
We added Lite Syntax for simplified schema creation for special cases, like to be used with reitit coercion and for easy migration from data-specs.
Example Address schema
Following example schema is assumed in many of the following examples.
(def Address
[:map
[:id :string]
[:tags [:set :keyword]]
[:address
[:map
[:street :string]
[:city :string]
[:zip :int]
[:lonlat [:tuple :double :double]]]]])
Validation
Validating values against a schema:
;; with schema instances
(m/validate (m/schema :int) 1)
;; => true
;; with vector syntax
(m/validate :int 1)
;; => true
(m/validate :int "1")
;; => false
(m/validate [:= 1] 1)
;; => true
(m/validate [:enum 1 2] 1)
;; => true
(m/validate [:and :int [:> 6]] 7)
;; => true
(m/validate [:qualified-keyword {:namespace :aaa}] :aaa/bbb)
;; => true
;; optimized (pure) validation function for
