Clojure
made by https://0x3d.site
GitHub - metosin/malli: High-performance data-driven data specification library for Clojure/Script.High-performance data-driven data specification library for Clojure/Script. - metosin/malli
Visit Site
GitHub - metosin/malli: High-performance data-driven data specification library for Clojure/Script.
malli
Data-driven Schemas for Clojure/Script and babashka.
Metosin Open Source Status: Active. Stability: well matured alpha.
- 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 wtih 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.
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.
Malli is tested with the LTS releases Java 8, 11, 17 and 21.
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:
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 best performance
(def valid?
(m/validator
[:map
[:x :boolean]
[:y {:optional true} :int]
[:z :string]]))
(valid? {:x true, :z "kikka"})
; => true
Schemas can have properties:
(def Age
[:and
{:title "Age"
:description "It's an age"
:json-schema/example 20}
:int [:> 18]])
(m/properties Age)
; => {:title "Age"
; :description "It's an age"
; :json-schema/example 20}
Maps are open by default:
(m/validate
[:map [:x :int]]
{:x 1, :extra "key"})
; => true
Maps can be closed with :closed
property:
(m/validate
[:map {:closed true} [:x :int]]
{:x 1, :extra "key"})
; => false
Maps keys are not limited to keywords:
(m/validate
[:map
["status" [:enum "ok"]]
[1 :any]
[nil :any]
[::a :string]]
{"status" "ok"
1 'number
nil :yay
::a "properly awesome"})
; => true
Most core-predicates are mapped to Schemas:
(m/validate string? "kikka")
; => true
See the full list of default schemas.
Enumeration schemas
:enum
schemas [:enum V1 V2 ...]
represent an enumerated set of values V1 V2 ...
.
This mostly works as you'd expect, with values passing the schema if it is contained in the set and generators returning one of the values, shrinking to the left-most value.
There are some special cases to keep in mind around syntax. Since schema properties can be specified with a map or nil, enumerations starting with a map or nil must use slightly different syntax.
If your :enum
does not have properties, you must provide nil
as the properties.
[:enum nil {}] ;; singleton schema of {}
[:enum nil nil] ;; singleton schema of nil
If your :enum
has properties, the leading map with be interpreted as properties, not an enumerated value.
[:enum {:foo :bar} {}] ;; singleton schema of {}, with properties {:foo :bar}
[:enum {:foo :bar} nil] ;; singleton schema of nil, with properties {:foo :bar}
In fact, these syntax rules apply to all schemas, but :enum
is the most common schema where this is relevant so it deserves a special mention.
Qualified keys in a map
You can also use decomplected maps keys and values using registry references. References must be either qualified keywords or strings.
(m/validate
[:map {:registry {::id int?
::country string?}}
::id
[:name string?]
[::country {:optional true}]]
{::id 1
:name "kikka"})
; => true
Homogeneous maps
Other times, we use a map as a homogeneous index. In this case, all our key-value
pairs have the same type. For this use case, we can use the :map-of
schema.
(m/validate
[:map-of :string [:map [:lat number?] [:long number?]]]
{"oslo" {:lat 60 :long 11}
"helsinki" {:lat 60 :long 24}})
;; => true
Map with default schemas
Map schemas can define a special :malli.core/default
key to handle extra keys:
(m/validate
[:map
[:x :int]
[:y :int]
[::m/default [:map-of :int :int]]]
{:x 1, :y 2, 1 1, 2 2})
; => true
default branching can be arbitrarily nested:
(m/validate
[:map
[:x :int]
[::m/default [:map
[:y :int]
[::m/default [:map-of :int :int]]]]]
{:x 1, :y 2, 1 1, 2 2})
; => true
Seqable schemas
The :seqable
and :every
schemas describe seqable?
collections. They
differ in their handling of collections that are neither counted?
nor indexed?
, and their
parsers:
:seqable
parses its elements but:every
does not and returns the identical input, and- valid unparsed
:seqable
values lose the original collection type while:every
returns the identical input.
:seqable
validates the entire collection, while :every
checks only the
largest of :min
, (inc :max)
, and (::m/coll-check-limit options 101)
, or
the entire collection if the input is counted?
or indexed?
.
;; :seqable and :every validate identically with small, counted, or indexed collections.
(m/validate [:seqable :int] #{1 2 3})
;=> true
(m/validate [:seqable :int] [1 2 3])
;=> true
(m/validate [:seqable :int] (sorted-set 1 2 3))
;=> true
(m/validate [:seqable :int] (range 1000))
;=> true
(m/validate [:seqable :int] (conj (vec (range 1000)) nil))
;=> false
(m/validate [:every :int] #{1 2 3})
;=> true
(m/validate [:every :int] [1 2 3])
;=> true
(m/validate [:every :int] (sorted-set 1 2 3))
;=> true
(m/validate [:every :int] (vec (range 1000)))
;=> true
(m/validate [:every :int] (conj (vec (range 1000)) nil))
;=> false
;; for large uncounted and unindexed collections, :every only checks a certain length
(m/validate [:seqable :int] (concat (range 1000) [nil]))
;=> false
(m/validate [:every :int] (concat (range 1000) [nil]))
;=> true
Sequence schemas
You can use :sequential
to describe homogeneous sequential Clojure collections.
(m/validate [:sequential any?] (list "this" 'is :number 42))
;; => true
(m/validate [:sequential int?] [42 105])
;; => true
(m/validate [:sequential int?] #{42 105})
;; => false
Malli also supports sequence regexes (also called sequence expressions) like Seqexp and Spec.
The supported operators are :cat
& :catn
for concatenation / sequencing
(m/validate [:cat string? int?] ["foo" 0]) ; => true
(m/validate [:catn [:s string?] [:n int?]] ["foo" 0]) ; => true
:alt
& :altn
for alternatives
(m/validate [:alt keyword? string?] ["foo"]) ; => true
(m/validate [:altn [:kw keyword?] [:s string?]] ["foo"]) ; => true
and :?
, :*
, :+
& :repeat
for repetition:
(m/validate [:? int?] []) ; => true
(m/validate [:? int?] [1]) ; => true
(m/validate [:? int?] [1 2]) ; => false
(m/validate [:* int?] []) ; => true
(m/validate [:* int?] [1 2 3]) ; => true
(m/validate [:+ int?] []) ; => false
(m/validate [:+ int?] [1]) ; => true
(m/validate [:+ int?] [1 2 3]) ; => true
(m/validate [:repeat {:min 2, :max 4} int?] [1]) ; => false
(m/validate [:repeat {:min 2, :max 4} int?] [1 2]) ; => true
(m/validate [:repeat {:min 2, :max 4} int?] [1 2 3 4]) ; => true (:max is inclusive, as elsewhere in Malli)
(m/validate [:repeat {:min 2, :max 4} int?] [1 2 3 4 5]) ; => false
:catn
and :altn
allow naming the subsequences / alternatives
(m/explain
[:* [:catn [:prop string?] [:val [:altn [:s string?] [:b boolean?]]]]]
["-server" "foo" "-verbose" 11 "-user" "joe"])
;; => {:schema [:* [:map [:prop string?] [:val [:map [:s string?] [:b boolean?]]]]],
;; :value ["-server" "foo" "-verbose" 11 "-user" "joe"],
;; :errors ({:path [0 :val :s], :in [3], :schema string?, :value 11}
;; {:path [0 :val :b], :in [3], :schema boolean?, :value 11})}
while :cat
and :alt
just use numeric indices for paths:
(m/explain
[:* [:cat string? [:alt string? boolean?]]]
["-server" "foo" "-verbose" 11 "-user" "joe"])
;; => {:schema [:* [:cat string? [:alt string? boolean?]]],
;; :value ["-server" "foo" "-verbose" 11 "-user" "joe"],
;; :errors ({:path [0 1 0], :in [3], :schema string?, :value 11}
;; {:path [0 1 1], :in [3], :schema boolean?, :value 11})}
As all these examples show, the sequence expression (seqex) operators take any non-seqex child schema to
mean a sequence of one element that matches that schema. To force that behaviour for
a seqex child :schema
can be used:
(m/validate
[:cat [:= :names] [:schema [:* string?]] [:= :nums] [:schema [:* number?]]]
[:names ["a" "b"] :nums [1 2 3]])
; => true
;; whereas
(m/validate
[:cat [:= :names] [:* string?] [:= :nums] [:* number?]]
[:names "a" "b" :nums 1 2 3])
; => true
Although a lot of effort has gone into making the seqex implementation fast
(require '[clojure.spec.alpha :as s])
(require '[criterium.core :as cc])
(let [valid? (partial s/valid? (s/* int?))]
(cc/quick-bench (valid? (range 10)))) ; Execution time mean : 27µs
(let [valid? (m/validator [:* int?])]
(cc/quick-bench (valid? (range 10)))) ; Execution time mean : 2.7µs
it is always better to use less general tools whenever possible:
(let [valid? (partial s/valid? (s/coll-of int?))]
(cc/quick-bench (valid? (range 10)))) ; Execution time mean : 1.8µs
(let [valid? (m/validator [:sequential int?])]
(cc/quick-bench (valid? (range 10)))) ; Execution time mean : 0.12µs
Vector schemas
You can use :vector
to describe homogeneous Clojure vectors.
(m/validate [:vector int?] [1 2 3])
;; => true
(m/validate [:vector int?] (list 1 2 3))
;; => false
A :tuple
schema describes a fixed length Clojure vector of heterogeneous elements:
(m/validate [:tuple keyword? string? number?] [:bing "bang" 42])
;; => true
To create a vector schema based on a seqex, use :and
.
;; non-empty vector starting with a keyword
(m/validate [:and [:cat :keyword [:* :any]]
vector?]
[:a 1])
; => true
(m/validate [:and [:cat :keyword [:* :any]]
vector?]
(:a 1))
; => false
Note: To generate values from a vector seqex, see :and generation.
Set schemas
You can use :set
to describe homogeneous Clojure sets.
(m/validate [:set int?] #{42 105})
;; => true
(m/validate [:set int?] #{:a :b})
;; => false
String schemas
Using a predicate:
(m/validate string? "kikka")
Using :string
Schema:
(m/validate :string "kikka")
;; => true
(m/validate [:string {:min 1, :max 4}] "")
;; => false
Using regular expressions:
(m/validate #"a+b+c+" "abbccc")
;; => true
;; :re with string
(m/validate [:re ".{3,5}"] "abc")
;; => true
;; :re with regex
(m/validate [:re #".{3,5}"] "abc")
;; => true
;; NB: re-find semantics
(m/validate [:re #"\d{4}"] "1234567")
;; => true
;; anchor with ^...$ if you want to strictly match the whole string
(m/validate [:re #"^\d{4}$"] "1234567")
;; => false
Maybe schemas
Use :maybe
to express that an element should match some schema OR be nil
:
(m/validate [:maybe string?] "bingo")
;; => true
(m/validate [:maybe string?] nil)
;; => true
(m/validate [:maybe string?] :bingo)
;; => false
Fn schemas
:fn
allows any predicate function to be used:
(def my-schema
[:and
[:map
[:x int?]
[:y int?]]
[:fn (fn [{:keys [x y]}] (> x y))]])
(m/validate my-schema {:x 1, :y 0})
; => true
(m/validate my-schema {:x 1, :y 2})
; => false
Error messages
Detailed errors with m/explain
:
(m/explain
Address
{:id "Lillan"
:tags #{:artesan :coffee :hotel}
:address {:street "Ahlmanintie 29"
:city "Tampere"
:zip 33100
:lonlat [61.4858322, 23.7854658]}})
; => nil
(m/explain
Address
{:id "Lillan"
:tags #{:artesan "coffee" :garden}
:address {:street "Ahlmanintie 29"
:zip 33100
:lonlat [61.4858322, nil]}})
;{:schema [:map
; [:id string?]
; [:tags [:set keyword?]]
; [:address [:map
; [:street string?]
; [:city string?]
; [:zip int?]
; [:lonlat [:tuple double? double?]]]]],
; :value {:id "Lillan",
; :tags #{:artesan :garden "coffee"},
; :address {:street "Ahlmanintie 29"
; :zip 33100
; :lonlat [61.4858322 nil]}},
; :errors ({:path [:tags 0]
; :in [:tags 0]
; :schema keyword?
; :value "coffee"}
; {:path [:address :city],
; :in [:address :city],
; :schema [:map
; [:street string?]
; [:city string?]
; [:zip int?]
; [:lonlat [:tuple double? double?]]],
; :type :malli.core/missing-key}
; {:path [:address :lonlat 1]
; :in [:address :lonlat 1]
; :schema double?
; :value nil})}
Under :errors
, you get a list of errors with the following keys:
:path
, error location in Schema:in
, error location in value:schema
, schema in error:value
, value in error
(def Schema [:map [:x [:maybe [:tuple :string]]]])
(def value {:x [1]})
(def error (-> Schema
(m/explain value)
:errors
first))
error
;{:path [:x 0 0]
; :in [:x 0]
; :schema :string
; :value 1}
(get-in value (:in error))
; => 1
(mu/get-in Schema (:path error))
; => :string
Note! If you need error messages that serialize neatly to EDN/JSON, use malli.util/explain-data
instead.
Humanized error messages
Explain results can be humanized with malli.error/humanize
:
(require '[malli.error :as me])
(-> Address
(m/explain
{:id "Lillan"
:tags #{:artesan "coffee" :garden}
:address {:street "Ahlmanintie 29"
:zip 33100
:lonlat [61.4858322, nil]}})
(me/humanize))
;{:tags #{["should be a keyword"]}
; :address {:city ["missing required key"]
; :lonlat [nil ["should be a double"]]}}
Or if you already have a malli validation exception (e.g. in a catch form):
(require '[malli.error :as me])
(try
(m/validate Address {:not "an address"})
(catch Exception e
(-> e ex-data :data :explain me/humanize)))
Custom error messages
Error messages can be customized with :error/message
and :error/fn
properties:
(-> [:map
[:id int?]
[:size [:enum {:error/message "should be: S|M|L"}
"S" "M" "L"]]
[:age [:fn {:error/fn (fn [{:keys [value]} _] (str value ", should be > 18"))}
(fn [x] (and (int? x) (> x 18)))]]]
(m/explain {:size "XL", :age 10})
(me/humanize
{:errors (-> me/default-errors
(assoc ::m/missing-key {:error/fn (fn [{:keys [in]} _] (str "missing key " (last in)))}))}))
;{:id ["missing key :id"]
; :size ["should be: S|M|L"]
; :age ["10, should be > 18"]}
Messages can be localized:
(-> [:map
[:id int?]
[:size [:enum {:error/message {:en "should be: S|M|L"
:fi "pitäisi olla: S|M|L"}}
"S" "M" "L"]]
[:age [:fn {:error/fn {:en (fn [{:keys [value]} _] (str value ", should be > 18"))
:fi (fn [{:keys [value]} _] (str value ", pitäisi olla > 18"))}}
(fn [x] (and (int? x) (> x 18)))]]]
(m/explain {:size "XL", :age 10})
(me/humanize
{:locale :fi
:errors (-> me/default-errors
(assoc-in ['int? :error-message :fi] "pitäisi olla numero")
(assoc ::m/missing-key {:error/fn {:en (fn [{:keys [in]} _] (str "missing key " (last in)))
:fi (fn [{:keys [in]} _] (str "puuttuu avain " (last in)))}}))}))
;{:id ["puuttuu avain :id"]
; :size ["pitäisi olla: S|M|L"]
; :age ["10, pitäisi olla > 18"]}
Top-level humanized map-errors are under :malli/error
:
(-> [:and [:map
[:password string?]
[:password2 string?]]
[:fn {:error/message "passwords don't match"}
(fn [{:keys [password password2]}]
(= password password2))]]
(m/explain {:password "secret"
:password2 "faarao"})
(me/humanize))
; {:malli/error ["passwords don't match"]}
Errors can be targeted using :error/path
property:
(-> [:and [:map
[:password string?]
[:password2 string?]]
[:fn {:error/message "passwords don't match"
:error/path [:password2]}
(fn [{:keys [password password2]}]
(= password password2))]]
(m/explain {:password "secret"
:password2 "faarao"})
(me/humanize))
; {:password2 ["passwords don't match"]}
By default, only direct erroneous schema properties are used:
(-> [:map
[:foo {:error/message "entry-failure"} :int]] ;; here, :int fails, no error props
(m/explain {:foo "1"})
(me/humanize))
; => {:foo ["should be an integer"]}
Looking up humanized errors from parent schemas with custom :resolve
(BETA, subject to change):
(-> [:map
[:foo {:error/message "entry-failure"} :int]]
(m/explain {:foo "1"})
(me/humanize {:resolve me/-resolve-root-error}))
; => {:foo ["entry-failure"]}
Spell checking
For closed schemas, key spelling can be checked with:
(-> [:map [:address [:map [:street string?]]]]
(mu/closed-schema)
(m/explain
{:name "Lie-mi"
:address {:streetz "Hämeenkatu 14"}})
(me/with-spell-checking)
(me/humanize))
;{:address {:street ["missing required key"]
; :streetz ["should be spelled :street"]}
; :name ["disallowed key"]}
Values in error
Just to get parts of the value that are in error:
(-> Address
(m/explain
{:id "Lillan"
:tags #{:artesan "coffee" :garden "ground"}
:address {:street "Ahlmanintie 29"
:zip 33100
:lonlat [61.4858322, "23.7832851,17"]}})
(me/error-value))
;{:tags #{"coffee" "ground"}
; :address {:lonlat [nil "23.7832851,17"]}}
Masking irrelevant parts:
(-> Address
(m/explain
{:id "Lillan"
:tags #{:artesan "coffee" :garden "ground"}
:address {:street "Ahlmanintie 29"
:zip 33100
:lonlat [61.4858322, "23.7832851,17"]}})
(me/error-value {::me/mask-valid-values '...}))
;{:id ...
; :tags #{"coffee" "ground" ...}
; :address {:street ...
; :zip ...
; :lonlat [... "23.7832851,17"]}}
Pretty errors
There are two ways to get pretty errors:
Development mode
Start development mode:
((requiring-resolve 'malli.dev/start!))
Now, any exception thrown via malli.core/-fail!
is being captured and pretty printed before being thrown. Pretty printing is extendable using virhe.
Pretty Coercion:
Custom exception (with default layout):
Pretty printing in being backed by malli.dev.virhe/-format
multimethod using (-> exception (ex-data) :data)
as the default dispatch key. As fallback, exception class - or exception subclass can be used, e.g. the following will handle all java.sql.SQLException
and it's parent exceptions:
(require '[malli.dev.virhe :as v])
(defmethod v/-format java.sql.SQLException [e _ printer]
{:title "Exception thrown"
:body [:group
(v/-block "SQL Exception" (v/-color :string (ex-message e) printer) printer) :break :break
(v/-block "More information:" (v/-link "https://cljdoc.org/d/metosin/malli/CURRENT" printer) printer)]})
pretty/explain
For pretty development-time error printing, try malli.dev.pretty/explain
Value transformation
(require '[malli.transform :as mt])
Two-way schema-driven value transformations with m/decode
and m/encode
using a Transformer
instance.
Default Transformers include:
name | description |
---|---|
mt/string-transformer |
transform between strings and EDN |
mt/json-transformer |
transform between JSON and EDN |
mt/strip-extra-keys-transformer |
drop extra keys from maps |
mt/default-value-transformer |
applies default values from schema properties |
mt/key-transformer |
transforms map keys |
mt/collection-transformer |
conversion between collections (e.g. set -> vector) |
NOTE: the included transformers are best-effort, i.e. they won't throw on bad input, they will just pass the input value through unchanged. You should make sure your schema validation catches these non-transformed values. Custom transformers should follow the same idiom.
Simple usage:
(m/decode int? "42" mt/string-transformer)
; 42
(m/encode int? 42 mt/string-transformer)
; "42"
For performance, precompute the transformations with m/decoder
and m/encoder
:
(def decode (m/decoder int? mt/string-transformer))
(decode "42")
; 42
(def encode (m/encoder int? mt/string-transformer))
(encode 42)
; "42"
Coercion
For both decoding + validating the results (throwing exception on error), there is m/coerce
and m/coercer
:
(m/coerce :int "42" mt/string-transformer)
; 42
((m/coercer :int mt/string-transformer) "42")
; 42
(m/coerce :int "invalid" mt/string-transformer)
; =throws=> :malli.core/invalid-input {:value "invalid", :schema :int, :explain {:schema :int, :value "invalid", :errors ({:path [], :in [], :schema :int, :value "invalid"})}}
Coercion can be applied without transformer, doing just validation:
(m/coerce :int 42)
; 42
(m/coerce :int "42")
; =throws=> :malli.core/invalid-input {:value "42", :schema :int, :explain {:schema :int, :value "42", :errors ({:path [], :in [], :schema :int, :value "42"})}}
Exception-free coercion with continuation-passing style:
(m/coerce :int "fail" nil (partial prn "success:") (partial prn "error:"))
; =prints=> "error:" {:value "fail", :schema :int, :explain ...}
Advanced Transformations
Transformations are recursive:
(m/decode
Address
{:id "Lillan",
:tags ["coffee" "artesan" "garden"],
:address {:street "Ahlmanintie 29"
:city "Tampere"
:zip 33100
:lonlat [61.4858322 23.7854658]}}
mt/json-transformer)
;{:id "Lillan",
; :tags #{:coffee :artesan :garden},
; :address {:street "Ahlmanintie 29"
; :city "Tampere"
; :zip 33100
; :lonlat [61.4858322 23.7854658]}}
Transform map keys:
(m/encode
Address
{:id "Lillan",
:tags ["coffee" "artesan" "garden"],
:address {:street "Ahlmanintie 29"
:city "Tampere"
:zip 33100
:lonlat [61.4858322 23.7854658]}}
(mt/key-transformer {:encode name}))
;{"id" "Lillan",
; "tags" ["coffee" "artesan" "garden"],
; "address" {"street" "Ahlmanintie 29"
; "city" "Tampere"
; "zip" 33100
; "lonlat" [61.4858322 23.7854658]}}
Transforming homogenous :enum
or :=
s (supports automatic type detection of :keyword
, :symbol
, :int
and :double
):
(m/decode [:enum :kikka :kukka] "kukka" mt/string-transformer)
; => :kukka
Transformers can be composed with mt/transformer
:
(def strict-json-transformer
(mt/transformer
mt/strip-extra-keys-transformer
mt/json-transformer))
(m/decode
Address
{:id "Lillan",
:EVIL "LYN"
:tags ["coffee" "artesan" "garden"],
:address {:street "Ahlmanintie 29"
:DARK "ORKO"
:city "Tampere"
:zip 33100
:lonlat [61.4858322 23.7854658]}}
strict-json-transformer)
;{:id "Lillan",
; :tags #{:coffee :artesan :garden},
; :address {:street "Ahlmanintie 29"
; :city "Tampere"
; :zip 33100
; :lonlat [61.4858322 23.7854658]}}
Schema properties can be used to override default transformations:
(m/decode
[string? {:decode/string clojure.string/upper-case}]
"kerran" mt/string-transformer)
; => "KERRAN"
This works too:
(m/decode
[string? {:decode {:string clojure.string/upper-case}}]
"kerran" mt/string-transformer)
; => "KERRAN"
Decoders and encoders as interceptors (with :enter
and :leave
stages):
(m/decode
[string? {:decode/string {:enter clojure.string/upper-case}}]
"kerran" mt/string-transformer)
; => "KERRAN"
(m/decode
[string? {:decode/string {:enter #(str "olipa_" %)
:leave #(str % "_avaruus")}}]
"kerran" mt/string-transformer)
; => "olipa_kerran_avaruus"
To access Schema (and options) use :compile
:
(m/decode
[int? {:math/multiplier 10
:decode/math {:compile (fn [schema _]
(let [multiplier (:math/multiplier (m/properties schema))]
(fn [x] (* x multiplier))))}}]
12
(mt/transformer {:name :math}))
; => 120
Going crazy:
(m/decode
[:map
{:decode/math {:enter #(update % :x inc)
:leave #(update % :x (partial * 2))}}
[:x [int? {:decode/math {:enter (partial + 2)
:leave (partial * 3)}}]]]
{:x 1}
(mt/transformer {:name :math}))
; => {:x 24}
:and
accumulates the transformed value left-to-right.
(m/decode
[:and
[:string {:decode/string '{:enter #(str "1_" %), :leave #(str % "_2")}}]
[:string {:decode/string '{:enter #(str "3_" %), :leave #(str % "_4")}}]]
"kerran" mt/string-transformer)
;; => "3_1_kerran_2_4"
:or
transforms using the first successful schema, left-to-right.
(m/decode
[:or
[:string {:decode/string '{:enter #(str "1_" %), :leave #(str % "_2")}}]
[:string {:decode/string '{:enter #(str "3_" %), :leave #(str % "_4")}}]]
"kerran" mt/string-transformer)
;; => "1_kerran_2"
(m/decode
[:or
:map
[:string {:decode/string '{:enter #(str "3_" %), :leave #(str % "_4")}}]]
"kerran" mt/string-transformer)
;; => "3_kerran_4"
Proxy schemas like :merge
and :union
transform as if m/deref
ed.
(m/decode
[:merge
[:map [:name [:string {:default "kikka"}]] ]
[:map [:description {:optional true} [:string {:default "kikka"}]]]]
{}
{:registry (merge (mu/schemas) (m/default-schemas))}
(mt/default-value-transformer {::mt/add-optional-keys true}))
;; => {:name "kikka"
;; :description "kikka"}
To and from JSON
The m/encode
and m/decode
functions work on clojure data. To go
from clojure data to JSON, you need a JSON library like
jsonista. Additionally, since
m/decode
doesn't check the schema, you need to run m/validate
(or
m/explain
) if you want to make sure your data conforms to your
schema.
To JSON:
(def Tags
(m/schema [:map
{:closed true}
[:tags [:set :keyword]]]))
(jsonista.core/write-value-as-string
(m/encode Tags
{:tags #{:bar :quux}}
mt/json-transformer))
; => "{\"tags\":[\"bar\",\"quux\"]}"
From JSON without validation:
(m/decode Tags
(jsonista.core/read-value "{\"tags\":[\"bar\",[\"quux\"]]}"
jsonista.core/keyword-keys-object-mapper)
mt/json-transformer)
; => {:tags #{:bar ["quux"]}}
From JSON with validation:
(m/explain Tags
(m/decode Tags
(jsonista.core/read-value "{\"tags\":[\"bar\",[\"quux\"]]}"
jsonista.core/keyword-keys-object-mapper)
mt/json-transformer))
; => {:schema [:map {:closed true} [:tags [:set :keyword]]],
; :value {:tags #{:bar ["quux"]}},
; :errors ({:path [:tags 0], :in [:tags ["quux"]], :schema :keyword, :value ["quux"]})}
(m/validate Tags
(m/decode Tags
(jsonista.core/read-value "{\"tags\":[\"bar\",\"quux\"]}" ; <- note! no error
jsonista.core/keyword-keys-object-mapper)
mt/json-transformer))
; => true
For performance, it's best to prebuild the validator, decoder and explainer:
(def validate-Tags (m/validator Tags))
(def decode-Tags (m/decoder Tags mt/json-transformer))
(-> (jsonista.core/read-value "{\"tags\":[\"bar\",\"quux\"]}"
jsonista.core/keyword-keys-object-mapper)
decode-Tags
validate-Tags)
; => true
Default values
Applying default values:
(m/decode [:and {:default 42} int?] nil mt/default-value-transformer)
; => 42
With custom key and type defaults:
(m/decode
[:map
[:user [:map
[:name :string]
[:description {:ui/default "-"} :string]]]]
nil
(mt/default-value-transformer
{:key :ui/default
:defaults {:map (constantly {})
:string (constantly "")}}))
; => {:user {:name "", :description "-"}}
With custom function:
(m/decode
[:map
[:os [:string {:property "os.name"}]]
[:timezone [:string {:property "user.timezone"}]]]
{}
(mt/default-value-transformer
{:key :property
:default-fn (fn [_ x] (System/getProperty x))}))
; => {:os "Mac OS X", :timezone "Europe/Helsinki"}
Optional Keys are not added by default:
(m/decode
[:map
[:name [:string {:default "kikka"}]]
[:description {:optional true} [:string {:default "kikka"}]]]
{}
(mt/default-value-transformer))
; => {:name "kikka"}
Adding optional keys too via ::mt/add-optional-keys
option:
(m/decode
[:map
[:name [:string {:default "kikka"}]]
[:description {:optional true} [:string {:default "kikka"}]]]
{}
(mt/default-value-transformer {::mt/add-optional-keys true}))
; => {:name "kikka", :description "kikka"}
Single sweep of defaults & string encoding:
(m/encode
[:map {:default {}}
[:a [int? {:default 1}]]
[:b [:vector {:default [1 2 3]} int?]]
[:c [:map {:default {}}
[:x [int? {:default 42}]]
[:y int?]]]
[:d [:map
[:x [int? {:default 42}]]
[:y int?]]]
[:e int?]]
nil
(mt/transformer
mt/default-value-transformer
mt/string-transformer))
;{:a "1"
; :b ["1" "2" "3"]
; :c {:x "42"}}
Programming with schemas
(require '[malli.util :as mu])
Updating Schema properties:
(mu/update-properties [:vector int?] assoc :min 1)
; => [:vector {:min 1} int?]
Lifted clojure.core
function to work with schemas: select-keys
, dissoc
, get
, assoc
, update
, get-in
, assoc-in
, update-in
(mu/get-in Address [:address :lonlat])
; => [:tuple double? double?]
(mu/update-in Address [:address] mu/assoc :country [:enum "fi" "po"])
;[:map
; [:id string?]
; [:tags [:set keyword?]]
; [:address
; [:map [:street string?]
; [:city string?]
; [:zip int?]
; [:lonlat [:tuple double? double?]]
; [:country [:enum "fi" "po"]]]]]
(-> Address
(mu/dissoc :address)
(mu/update-properties assoc :title "Address"))
;[:map {:title "Address"}
; [:id string?]
; [:tags [:set keyword?]]]
Making keys optional or required:
(mu/optional-keys [:map [:x int?] [:y int?]])
;[:map
; [:x {:optional true} int?]
; [:y {:optional true} int?]]
(mu/required-keys [:map [:x {:optional true} int?] [:y int?]])
;[:map
; [:x int?]
; [:y int?]]
Closing and opening all :map
schemas recursively:
(def abcd
[:map {:title "abcd"}
[:a int?]
[:b {:optional true} int?]
[:c [:map
[:d int?]]]])
(mu/closed-schema abcd)
;[:map {:title "abcd", :closed true}
; [:a int?]
; [:b {:optional true} int?]
; [:c [:map {:closed true}
; [:d int?]]]]
(-> abcd
mu/closed-schema
mu/open-schema)
;[:map {:title "abcd"}
; [:a int?]
; [:b {:optional true} int?]
; [:c [:map
; [:d int?]]]]
Merging Schemas (last value wins):
(mu/merge
[:map
[:name string?]
[:description string?]
[:address
[:map
[:street string?]
[:country [:enum "finland" "poland"]]]]]
[:map
[:description {:optional true} string?]
[:address
[:map
[:country string?]]]])
;[:map
; [:name string?]
; [:description {:optional true} string?]
; [:address [:map
; [:street string?]
; [:country string?]]]]
With :and
, first child is used in merge:
(mu/merge
[:and {:type "entity"}
[:map {:title "user"}
[:name :string]]
map?]
[:map {:description "aged"} [:age :int]])
;[:and {:type "entity"}
; [:map {:title "user", :description "aged"}
; [:name :string]
; [:age :int]]
; map?]
Schema unions (merged values of both schemas are valid for union schema):
(mu/union
[:map
[:name string?]
[:description string?]
[:address
[:map
[:street string?]
[:country [:enum "finland" "poland"]]]]]
[:map
[:description {:optional true} string?]
[:address
[:map
[:country string?]]]])
;[:map
; [:name string?]
; [:description {:optional true} string?]
; [:address [:map
; [:street string?]
; [:country [:or [:enum "finland" "poland"] string?]]]]]
Adding generated example values to Schemas:
(m/walk
[:map
[:name string?]
[:description string?]
[:address
[:map
[:street string?]
[:country [:enum "finland" "poland"]]]]]
(m/schema-walker
(fn [schema]
(mu/update-properties schema assoc :examples (mg/sample schema {:size 2, :seed 20})))))
;[:map
; {:examples ({:name "", :description "", :address {:street "", :country "poland"}}
; {:name "W", :description "x", :address {:street "8", :country "finland"}})}
; [:name [string? {:examples ("" "")}]]
; [:description [string? {:examples ("" "")}]]
; [:address
; [:map
; {:examples ({:street "", :country "finland"} {:street "W", :country "poland"})}
; [:street [string? {:examples ("" "")}]]
; [:country [:enum {:examples ("finland" "poland")} "finland" "poland"]]]]]
Finding first value (prewalk):
(mu/find-first
[:map
[:x int?]
[:y [:vector [:tuple
[:or [:and {:salaisuus "turvassa"} boolean?] int?]
[:schema {:salaisuus "vaarassa"} false?]]]]
[:z [:string {:salaisuus "piilossa"}]]]
(fn [schema _ _]
(-> schema m/properties :salaisuus)))
; => "turvassa"
Finding all subschemas with paths, retaining order:
(def Schema
(m/schema
[:maybe
[:map
[:id string?]
[:tags [:set keyword?]]
[:address
[:and
[:map
[:street {:optional true} string?]
[:lonlat {:optional true} [:tuple double? double?]]]
[:fn (fn [{:keys [street lonlat]}] (or street lonlat))]]]]]))
(mu/subschemas Schema)
;[{:path [], :in [], :schema [:maybe
; [:map
; [:id string?]
; [:tags [:set keyword?]]
; [:address
; [:and
; [:map
; [:street {:optional true} string?]
; [:lonlat {:optional true} [:tuple double? double?]]]
; [:fn (fn [{:keys [street lonlat]}] (or street lonlat))]]]]]}
; {:path [0], :in [], :schema [:map
; [:id string?]
; [:tags [:set keyword?]]
; [:address
; [:and
; [:map
; [:street {:optional true} string?]
; [:lonlat {:optional true} [:tuple double? double?]]]
; [:fn (fn [{:keys [street lonlat]}] (or street lonlat))]]]]}
; {:path [0 :id], :in [:id], :schema string?}
; {:path [0 :tags], :in [:tags], :schema [:set keyword?]}
; {:path [0 :tags :malli.core/in], :in [:tags :malli.core/in], :schema keyword?}
; {:path [0 :address], :in [:address], :schema [:and
; [:map
; [:street {:optional true} string?]
; [:lonlat {:optional true} [:tuple double? double?]]]
; [:fn (fn [{:keys [street lonlat]}] (or street lonlat))]]}
; {:path [0 :address 0], :in [:address], :schema [:map
; [:street {:optional true} string?]
; [:lonlat {:optional true} [:tuple double? double?]]]}
; {:path [0 :address 0 :street], :in [:address :street], :schema string?}
; {:path [0 :address 0 :lonlat], :in [:address :lonlat], :schema [:tuple double? double?]}
; {:path [0 :address 0 :lonlat 0], :in [:address :lonlat 0], :schema double?}
; {:path [0 :address 0 :lonlat 1], :in [:address :lonlat 1], :schema double?}
; {:path [0 :address 1], :in [:address], :schema [:fn (fn [{:keys [street lonlat]}] (or street lonlat))]}]
Collecting unique value paths and their schema paths:
(->> Schema
(mu/subschemas)
(mu/distinct-by :id)
(mapv (juxt :in :path)))
;[[[] []]
; [[] [0]]
; [[:id] [0 :id]]
; [[:tags] [0 :tags]]
; [[:tags :malli.core/in] [0 :tags :malli.core/in]]
; [[:address] [0 :address]]
; [[:address] [0 :address 0]]
; [[:address :street] [0 :address 0 :street]]
; [[:address :lonlat] [0 :address 0 :lonlat]]
; [[:address :lonlat 0] [0 :address 0 :lonlat 0]]
; [[:address :lonlat 1] [0 :address 0 :lonlat 1]]
; [[:address] [0 :address 1]]]
Schema paths can be converted into value paths:
(mu/get-in Schema [0 :address 0 :lonlat])
; => [:tuple double? double?]
(mu/path->in Schema [0 :address 0 :lonlat])
; => [:address :lonlat]
and back, returning all paths:
(mu/in->paths Schema [:address :lonlat])
; => [[0 :address 0 :lonlat]]
Declarative schema transformation
There are also declarative versions of schema transforming utilities in malli.util/schemas
. These include :merge
, :union
and :select-keys
:
(def registry (merge (m/default-schemas) (mu/schemas)))
(def Merged
(m/schema
[:merge
[:map [:x :string]]
[:map [:y :int]]]
{:registry registry}))
Merged
;[:merge
; [:map [:x :string]]
; [:map [:y :int]]]
(m/deref Merged)
;[:map
; [:x :string]
; [:y :int]]
(m/validate Merged {:x "kikka", :y 6})
; => true
:union
is similar to :or
, except :union
combines map schemas in different disjuncts with :or
.
For example, UnionMaps
is equivalent to [:map [:x [:or :int :string]] [:y [:or :int :string]]]
.
(def OrMaps
(m/schema
[:or
[:map [:x :int] [:y :string]]
[:map [:x :string] [:y :int]]]
{:registry registry}))
(def UnionMaps
(m/schema
[:union
[:map [:x :int] [:y :string]]
[:map [:x :string] [:y :int]]]
{:registry registry}))
(m/validate OrMaps {:x "kikka" :y "kikka"})
; => false
(m/validate UnionMaps {:x "kikka" :y "kikka"})
; => true
:merge
and :union
differ on schemas with common keys. :merge
chooses the right-most
schema of common keys, and :union
combines them with :or
.
For example, MergedCommon
is equivalent to [:map [:x :int]]
, and UnionCommon
is equivalent to [:map [:x [:or :string :int]]]
.
(def MergedCommon
(m/schema
[:merge
[:map [:x :string]]
[:map [:x :int]]]
{:registry registry}))
(def UnionCommon
(m/schema
[:union
[:map [:x :string]]
[:map [:x :int]]]
{:registry registry}))
(m/validate MergedCommon {:x "kikka"})
; => true
(m/validate MergedCommon {:x 1})
; => false
(m/validate UnionCommon {:x "kikka"})
; => true
(m/validate UnionCommon {:x 1})
; => true
Distributive schemas
:merge
also distributes over :multi
in a similar way to how multiplication
distributes over addition in arithmetic. There are two transformation rules, applied in the following order:
;; right-distributive
[:merge [:multi M1 M2 ...] M3]
=>
[:multi [:merge M1 M3] [:merge M2 M3] ...]
;; left-distributive
[:merge M1 [:multi M2 M3 ...]]
=>
[:multi [:merge M1 M2] [:merge M1 M3] ...]
For :merge
with more than two arguments, the rules are applied iteratively left-to-right
as if the following transformation was applied:
[:merge M1 M2 M3 M4 ...]
=>
[:merge
[:merge
[:merge M1 M2]
M3]
M4]
...
The distributive property of :multi
is useful combined with :merge
if you want all clauses of a :multi
to share extra entries.
Here are concrete examples of applying the rules:
;; left-distributive
(m/deref
[:merge
[:map [:x :int]]
[:multi {:dispatch :y}
[1 [:map [:y [:= 1]]]]
[2 [:map [:y [:= 2]]]]]]
{:registry registry})
; => [:multi {:dispatch :y}
; [1 [:map [:x :int] [:y [:= 1]]]]
; [2 [:map [:x :int] [:y [:= 2]]]]]
;; right-distributive
(m/deref
[:merge
[:multi {:dispatch :y}
[1 [:map [:y [:= 1]]]]
[2 [:map [:y [:= 2]]]]]
[:map [:x :int]]]
{:registry registry})
; => [:multi {:dispatch :y}
; [1 [:map [:y [:= 1]] [:x :int]]]
; [2 [:map [:y [:= 2]] [:x :int]]]]
It is not recommended to use local registries in schemas that are transformed.
Also be aware that merging non-maps via the distributive property inherits
the same semantics as :merge
, which is based on meta-merge.
Persisting schemas
Writing and Reading schemas as EDN, no eval
needed.
Following example requires SCI or cherry as external dependency because it includes a (quoted) function definition. See Serializable functions.
(require '[malli.edn :as edn])
(-> [:and
[:map
[:x int?]
[:y int?]]
[:fn '(fn [{:keys [x y]}] (> x y))]]
(edn/write-string)
(doto prn) ; => "[:and [:map [:x int?] [:y int?]] [:fn (fn [{:keys [x y]}] (> x y))]]"
(edn/read-string)
(doto (-> (m/validate {:x 0, :y 1}) prn)) ; => false
(doto (-> (m/validate {:x 2, :y 1}) prn))) ; => true
;[:and
; [:map
; [:x int?]
; [:y int?]]
; [:fn (fn [{:keys [x y]}] (> x y))]]
Multi schemas
Closed dispatch with :multi
schema and :dispatch
property:
(m/validate
[:multi {:dispatch :type}
[:sized [:map [:type keyword?] [:size int?]]]
[:human [:map [:type keyword?] [:name string?] [:address [:map [:country keyword?]]]]]]
{:type :sized, :size 10})
; true
Default branch with ::m/default
:
(def valid?
(m/validator
[:multi {:dispatch :type}
["object" [:map-of :keyword :string]]
[::m/default :string]]))
(valid? {:type "object", :key "1", :value "100"})
; => true
(valid? "SUCCESS!")
; => true
(valid? :failure)
; => false
Any function can be used for :dispatch
:
(m/validate
[:multi {:dispatch first}
[:sized [:tuple keyword? [:map [:size int?]]]]
[:human [:tuple keyword? [:map [:name string?] [:address [:map [:country keyword?]]]]]]]
[:human {:name "seppo", :address {:country :sweden}}])
; true
:dispatch
values should be decoded before actual values:
(m/decode
[:multi {:dispatch :type
:decode/string #(update % :type keyword)}
[:sized [:map [:type [:= :sized]] [:size int?]]]
[:human [:map [:type [:= :human]] [:name string?] [:address [:map [:country keyword?]]]]]]
{:type "human"
:name "Tiina"
:age "98"
:address {:country "finland"
:street "this is an extra key"}}
(mt/transformer mt/strip-extra-keys-transformer mt/string-transformer))
;{:type :human
; :name "Tiina"
; :address {:country :finland}}
Recursive schemas
To create a recursive schema, introduce a local registry and wrap all recursive positions in the registry with :ref
. Now you may reference the recursive schemas in the body of the schema.
For example, here is a recursive schema using :schema
for singly-linked lists of positive integers:
(m/validate
[:schema {:registry {::cons [:maybe [:tuple pos-int? [:ref ::cons]]]}}
[:ref ::cons]]
[16 [64 [26 [1 [13 nil]]]]])
; => true
Without the :ref
keyword, malli eagerly expands the schema until a stack overflow error is thrown:
(m/validate
[:schema {:registry {::cons [:maybe [:tuple pos-int? ::cons]]}}
::cons]
[16 [64 [26 [1 [13 nil]]]]])
; StackOverflowError
Technically, you only need the :ref
in recursive positions. However, it is best practice to :ref
all references
to recursive variables for better-behaving generators:
;; Note:
[:schema {:registry {::cons [:maybe [:tuple pos-int? [:ref ::cons]]]}}
::cons]
;; produces the same generator as the "unfolded"
[:maybe [:tuple pos-int? [:schema {:registry {::cons [:maybe [:tuple pos-int? [:ref ::cons]]]}} ::cons]]]
;; while
[:schema {:registry {::cons [:maybe [:tuple pos-int? [:ref ::cons]]]}}
[:ref ::cons]]
;; has a direct correspondance to the following generator:
(gen/recursive-gen
(fn [rec] (gen/one-of [(gen/return nil) (gen/tuple rec)]))
(gen/return nil))
Mutual recursion works too. Thanks to the :schema
construct, many schemas could be defined in the local registry, the top-level one being promoted by the :schema
second parameter:
(m/validate
[:schema {:registry {::ping [:maybe [:tuple [:= "ping"] [:ref ::pong]]]
::pong [:maybe [:tuple [:= "pong"] [:ref ::ping]]]}}
::ping]
["ping" ["pong" ["ping" ["pong" ["ping" nil]]]]])
; => true
Nested registries, the last definition wins:
(m/validate
[:schema {:registry {::ping [:maybe [:tuple [:= "ping"] [:ref ::pong]]]
::pong any?}} ;; effectively unreachable
[:schema {:registry {::pong [:maybe [:tuple [:= "pong"] [:ref ::ping]]]}}
::ping]]
["ping" ["pong" ["ping" ["pong" ["ping" nil]]]]])
; => true
Value generation
Schemas can be used to generate values:
(require '[malli.generator :as mg])
;; random
(mg/generate keyword?)
; => :?
;; using seed
(mg/generate [:enum "a" "b" "c"] {:seed 42})
;; => "a"
;; using seed and size
(mg/generate pos-int? {:seed 10, :size 100})
;; => 55740
;; regexs work too (only clj and if [com.gfredericks/test.chuck "0.2.10"+] available)
(mg/generate
[:re #"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$"]
{:seed 42, :size 10})
; => "[email protected]"
;; :gen/return (note, not validated)
(mg/generate
[:and {:gen/return 42} :int])
; => 42
;; :gen/elements (note, are not validated)
(mg/generate
[:and {:gen/elements ["kikka" "kukka" "kakka"]} string?]
{:seed 10})
; => "kikka"
;; :gen/fmap
(mg/generate
[:and {:gen/fmap (partial str "kikka_")} string?]
{:seed 10, :size 10})
;; => "kikka_WT3K0yax2"
;; portable :gen/fmap (requires `org.babashka/sci` dependency to work)
(mg/generate
[:and {:gen/fmap '(partial str "kikka_")} string?]
{:seed 10, :size 10})
;; => "kikka_nWT3K0ya7"
;; :gen/schema
(mg/generate
[:any {:gen/schema [:int {:min 10, :max 20}]}]
{:seed 10})
; => 19
;; :gen/min & :gen/max for numbers and collections
(mg/generate
[:vector {:gen/min 4, :gen/max 4} :int]
{:seed 1})
; => [-8522515 -1433 -1 1]
;; :gen/infinite? & :gen/NaN? for :double
(mg/generate
[:double {:gen/infinite? true, :gen/NaN? true}]
{:seed 1})
; => ##Inf
(require '[clojure.test.check.generators :as gen])
;; gen/gen (note, not serializable)
(mg/generate
[:sequential {:gen/gen (gen/list gen/neg-int)} int?]
{:size 42, :seed 42})
; => (-37 -13 -13 -24 -20 -11 -34 -40 -22 0 -10)
Generated values are valid:
(mg/generate Address {:seed 123, :size 4})
;{:id "H7",
; :tags #{:v?.w.t6!.QJYk-/-?s*4
; :_7U
; :QdG/Xi8J
; :*Q-.p*8*/n-J9u}
; :address {:street "V9s"
; :city ""
; :zip 3
; :lonlat [-2.75 -0.625]}}
(m/validate Address (mg/generate Address))
; => true
Sampling values:
;; sampling
(mg/sample [:and int? [:> 10] [:< 100]] {:seed 123})
; => (25 39 51 13 53 43 57 15 26 27)
Integration with test.check:
(require '[clojure.test.check.generators :as gen])
(gen/sample (mg/generator pos-int?))
; => (2 1 2 2 2 2 8 1 55 83)
:and generation
Generators for :and
schemas work by generating values from the first child, and then filtering
out any values that do not pass the overall :and
schema.
For the most reliable results, place the schema that is most likely to generate valid
values for the entire schema as the first child of an :and
schema.
;; BAD: :string is unlikely to generate values satisfying the schema
(mg/generate [:and :string [:enum "a" "b" "c"]] {:seed 42})
; Execution error
; Couldn't satisfy such-that predicate after 100 tries.
;; GOOD: every value generated by the `:enum` is a string
(mg/generate [:and [:enum "a" "b" "c"] :string] {:seed 42})
; => "a"
You might need to customize the generator for the first :and
child to improve
the chances of it generating valid values.
For example, a schema for non-empty heterogeneous vectors can validate values
by combining :cat
and vector?
, but since :cat
generates sequences
we need to use :gen/fmap
to make it generate vectors:
;; generate a non-empty vector starting with a keyword
(mg/generate [:and [:cat {:gen/fmap vec}
:keyword [:* :any]]
vector?]
{:size 1
:seed 2})
;=> [:.+ [1]]
Inferring schemas
Inspired by F# Type providers:
(require '[malli.provider :as mp])
(def samples
[{:id "Lillan"
:tags #{:artesan :coffee :hotel}
:address {:street "Ahlmanintie 29"
:city "Tampere"
:zip 33100
:lonlat [61.4858322, 23.7854658]}}
{:id "Huber",
:description "Beefy place"
:tags #{:beef :wine :beer}
:address {:street "Aleksis Kiven katu 13"
:city "Tampere"
:zip 33200
:lonlat [61.4963599 23.7604916]}}])
(mp/provide samples)
;[:map
; [:id :string]
; [:tags [:set :keyword]]
; [:address
; [:map
; [:street :string]
; [:city :string]
; [:zip :int]
; [:lonlat [:vector :double]]]]
; [:description {:optional true} :string]]
All samples are valid against the inferred schema:
(every? (partial m/validate (mp/provide samples)) samples)
; => true
For better performance, use mp/provider
:
(require '[criterium.core :as p])
;; 5ms
(p/bench (mp/provide samples))
;; 500µs (10x)
(let [provider (mp/provider)]
(p/bench (provider samples)))
:map-of inferring
By default, :map-of
is not inferred:
(mp/provide
[{"1" [1]}
{"2" [1 2]}
{"3" [1 2 3]}])
;[:map
; ["1" {:optional true} [:vector :int]]
; ["2" {:optional true} [:vector :int]]
; ["3" {:optional true} [:vector :int]]]
With ::mp/map-of-threshold
option:
(mp/provide
[{"1" [1]}
{"2" [1 2]}
{"3" [1 2 3]}]
{::mp/map-of-threshold 3})
; [:map-of :string [:vector :int]]
Sample-data can be type-hinted with ::mp/hint
:
(mp/provide
[^{::mp/hint :map-of}
{:a {:b 1, :c 2}
:b {:b 2, :c 1}
:c {:b 3}
:d nil}])
;[:map-of
; :keyword
; [:maybe [:map
; [:b :int]
; [:c {:optional true} :int]]]]
:tuple inferring
By default, tuples are not inferred:
(mp/provide
[[1 "kikka" true]
[2 "kukka" true]
[3 "kakka" true]])
; [:vector :some]
With ::mp/tuple-threshold
option:
(mp/provide
[[1 "kikka" true]
[2 "kukka" true]
[3 "kakka" false]]
{::mp/tuple-threshold 3})
; [:tuple :int :string :boolean]
Sample-data can be type-hinted with ::mp/hint
:
(mp/provide
[^{::mp/hint :tuple}
[1 "kikka" true]
["2" "kukka" true]])
; [:tuple :some string? boolean?]
value decoding in inferring
By default, no decoding is applied for (leaf) values:
(mp/provide
[{:id "caa71a26-5fe1-11ec-bf63-0242ac130002"}
{:id "8aadbf5e-5fe3-11ec-bf63-0242ac130002"}])
; => [:map [:id string?]]
Adding custom decoding via ::mp/value-decoders
option:
(mp/provide
[{:id "caa71a26-5fe1-11ec-bf63-0242ac130002"
:time "2021-01-01T00:00:00Z"}
{:id "8aadbf5e-5fe3-11ec-bf63-0242ac130002"
:time "2022-01-01T00:00:00Z"}]
{::mp/value-decoders {:string {:uuid mt/-string->uuid
'inst? mt/-string->date}}})
; => [:map [:id :uuid] [:time inst?]
Destructuring
Schemas can also be inferred from Clojure Destructuring Syntax.
(require '[malli.destructure :as md])
(def infer (comp :schema md/parse))
(infer '[a b & cs])
; => [:cat :any :any [:* :any]]
Malli also supports adding type hints as an extension to the normal Clojure syntax (enabled by default), inspired by Plumatic Schema.
(infer '[a :- :int, b :- :string & cs :- [:* :boolean]])
; => [:cat :int :string [:* :boolean]]
Pulling out function argument schemas from Vars:
(defn kikka
([a] [a])
([a b & cs] [a b cs]))
(md/infer #'kikka)
;[:function
; [:=> [:cat :any] :any]
; [:=> [:cat :any :any [:* :any]] :any]]
md/parse
uses the following options:
key | description |
---|---|
::md/inline-schemas |
support plumatic-style inline schemas (true) |
::md/sequential-maps |
support sequential maps in non-rest position (true) |
::md/references |
qualified schema references used (true) |
::md/required-keys |
destructured keys are required (false) |
::md/closed-maps |
destructured maps are closed (false) |
A more complete example:
(infer '[a [b c & rest :as bc]
& {:keys [d e]
:demo/keys [f]
g :demo/g
[h] :h
:or {d 0}
:as opts}])
;[:cat
; :any
; [:maybe [:cat
; [:? :any]
; [:? :any]
; [:* :any]]]
; [:altn
; [:map
; [:map
; [:d {:optional true} :any]
; [:e {:optional true} :any]
; [:demo/f {:optional true}]
; [:demo/g {:optional true}]
; [:h {:optional true} [:maybe [:cat
; [:? :any]
; [:* :any]]]]]]
; [:args
; [:*
; [:alt
; [:cat [:= :d] :any]
; [:cat [:= :e] :any]
; [:cat [:= :demo/f] :demo/f]
; [:cat [:= :demo/g] :demo/g]
; [:cat [:= :h] [:maybe [:cat
; [:? :any]
; [:* :any]]]]
; [:cat :any :any]]]]]]
Parsing values
Schemas can be used to parse values using m/parse
and m/parser
:
m/parse
for one-time things:
(m/parse
[:* [:catn
[:prop string?]
[:val [:altn
[:s string?]
[:b boolean?]]]]]
["-server" "foo" "-verbose" true "-user" "joe"])
;[{:prop "-server", :val [:s "foo"]}
; {:prop "-verbose", :val [:b true]}
; {:prop "-user", :val [:s "joe"]}]
m/parser
to create an optimized parser:
(def Hiccup
[:schema {:registry {"hiccup" [:orn
[:node [:catn
[:name keyword?]
[:props [:? [:map-of keyword? any?]]]
[:children [:* [:schema [:ref "hiccup"]]]]]]
[:primitive [:orn
[:nil nil?]
[:boolean boolean?]
[:number number?]
[:text string?]]]]}}
"hiccup"])
(def parse-hiccup (m/parser Hiccup))
(parse-hiccup
[:div {:class [:foo :bar]}
[:p "Hello, world of data"]])
;[:node
; {:name :div
; :props {:class [:foo :bar]}
; :children [[:node
; {:name :p
; :props nil
; :children [[:primitive [:text "Hello, world of data"]]]}]]}]
Parsing returns tagged values for :orn
, :catn
, :altn
and :multi
.
(def Multi
[:multi {:dispatch :type}
[:user [:map [:size :int]]]
[::m/default :any]])
(m/parse Multi {:type :user, :size 1})
; => [:user {:type :user, :size 1}]
(m/parse Multi {:type "sized", :size 1})
; => [:malli.core/default {:type "sized", :size 1}]
Unparsing values
The inverse of parsing, using m/unparse
and m/unparser
:
(->> [:div {:class [:foo :bar]}
[:p "Hello, world of data"]]
(m/parse Hiccup)
(m/unparse Hiccup))
;[:div {:class [:foo :bar]}
; [:p "Hello, world of data"]]
Serializable functions
Enabling serializable function schemas requires SCI or cherry (for client side) as external dependency. If
it is not present, the malli function evaluator throws :sci-not-available
exception.
For ClojureScript, you need to require sci.core
or malli.cherry
manually.
For GraalVM, you need to require sci.core
manually, before requiring any malli namespaces.
(def my-schema
[:and
[:map
[:x int?]
[:y int?]]
[:fn '(fn [{:keys [x y]}] (> x y))]])
(m/validate my-schema {:x 1, :y 0})
; => true
(m/validate my-schema {:x 1, :y 2})
; => false
NOTE: sci is not termination safe so be wary of sci
functions from untrusted sources. You can explicitly disable sci with option ::m/disable-sci
and set the default options with ::m/sci-options
.
(m/validate [:fn 'int?] 1 {::m/disable-sci true})
; Execution error
; :malli.core/sci-not-available {:code int?}
Schema AST
Implemented with protocol malli.core/AST
. Allows lossless round-robin with faster schema creation.
NOTE: For now, the AST syntax in considered as internal, e.g. don't use it as a database persistency model.
(def ?schema
[:map
[:x boolean?]
[:y {:optional true} int?]
[:z [:map
[:x boolean?]
[:y {:optional true} int?]]]])
(m/form ?schema)
;[:map
; [:x boolean?]
; [:y {:optional true} int?]
; [:z [:map
; [:x boolean?]
; [:y {:optional true} int?]]]]
(m/ast ?schema)
;{:type :map,
; :keys {:x {:order 0
; :value {:type boolean?}},
; :y {:order 1, :value {:type int?}
; :properties {:optional true}},
; :z {:order 2,
; :value {:type :map,
; :keys {:x {:order 0
; :value {:type boolean?}},
; :y {:order 1
; :value {:type int?}
; :properties {:optional true}}}}}}}
(-> ?schema
(m/schema) ;; 3.4µs
(m/ast)
(m/from-ast) ;; 180ns (18x, lazy)
(m/form)
(= (m/form ?schema)))
; => true
Schema transformation
Schemas can be transformed using post-walking, e.g. the Visitor Pattern.
The identity walker:
(m/walk
Address
(m/schema-walker identity))
;[:map
; [:id string?]
; [:tags [:set keyword?]]
; [:address
; [:map
; [:street string?]
; [:city string?]
; [:zip int?]
; [:lonlat [:tuple double? double?]]]]]
Adding :title
property to schemas:
(m/walk
Address
(m/schema-walker #(mu/update-properties % assoc :title (name (m/type %)))))
;[:map {:title "map"}
; [:id [string? {:title "string?"}]]
; [:tags [:set {:title "set"} [keyword? {:title "keyword?"}]]]
; [:address
; [:map {:title "map"}
; [:street [string? {:title "string?"}]]
; [:city [string? {:title "string?"}]]
; [:zip [int? {:title "int?"}]]
; [:lonlat [:tuple {:title "tuple"} [double? {:title "double?"}] [double? {:title "double?"}]]]]]]
Transforming schemas into maps:
(m/walk
Address
(fn [schema _ children _]
(-> (m/properties schema)
(assoc :malli/type (m/type schema))
(cond-> (seq children) (assoc :malli/children children)))))
;{:malli/type :map,
; :malli/children [[:id nil {:malli/type string?}]
; [:tags nil {:malli/type :set
; :malli/children [{:malli/type keyword?}]}]
; [:address nil {:malli/type :map,
; :malli/children [[:street nil {:malli/type string?}]
; [:city nil {:malli/type string?}]
; [:zip nil {:malli/type int?}]
; [:lonlat nil {:malli/type :tuple
; :malli/children [{:malli/type double?}
; {:malli/type double?}]}]]}]]}
JSON Schema
Transforming Schemas into JSON Schema:
(require '[malli.json-schema :as json-schema])
(json-schema/transform Address)
;{:type "object",
; :properties {:id {:type "string"},
; :tags {:type "array"
; :items {:type "string"}
; :uniqueItems true},
; :address {:type "object",
; :properties {:street {:type "string"},
; :city {:type "string"},
; :zip {:type "integer", :format "int64"},
; :lonlat {:type "array",
; :items [{:type "number"} {:type "number"}],
; :additionalItems false}},
; :required [:street :city :zip :lonlat]}},
; :required [:id :tags :address]}
Custom transformation via :json-schema
namespaced properties:
(json-schema/transform
[:enum
{:title "Fish"
:description "It's a fish"
:json-schema/type "string"
:json-schema/default "perch"}
"perch" "pike"])
;{:title "Fish"
; :description "It's a fish"
; :type "string"
; :default "perch"
; :enum ["perch" "pike"]}
Full override with :json-schema
property:
(json-schema/transform
[:map {:json-schema {:type "file"}}
[:file any?]])
; {:type "file"}
Swagger2
Transforming Schemas into Swagger2 Schema:
(require '[malli.swagger :as swagger])
(swagger/transform Address)
;{:type "object",
; :properties {:id {:type "string"},
; :tags {:type "array"
; :items {:type "string"}
; :uniqueItems true},
; :address {:type "object",
; :properties {:street {:type "string"},
; :city {:type "string"},
; :zip {:type "integer", :format "int64"},
; :lonlat {:type "array",
; :items {},
; :x-items [{:type "number", :format "double"}
; {:type "number", :format "double"}]}},
; :required [:street :city :zip :lonlat]}},
; :required [:id :tags :address]}
Custom transformation via :swagger
and :json-schema
namespaced properties:
(swagger/transform
[:enum
{:title "Fish"
:description "It's a fish"
:swagger/type "string"
:json-schema/default "perch"}
"perch" "pike"])
;{:title "Fish"
; :description "It's a fish"
; :type "string"
; :default "perch"
; :enum ["perch" "pike"]}
Full override with :swagger
property:
(swagger/transform
[:map {:swagger {:type "file"}}
[:file any?]])
; {:type "file"}
Custom schema types
Schema Types are described using m/IntoSchema
protocol, which has a factory method
(-into-schema [this properties children options])
to create the actual Schema instances.
See malli.core
for example implementations.
Simple schema
For simple cases, there is m/-simple-schema
:
(require '[clojure.test.check.generators :as gen])
(def Over6
(m/-simple-schema
{:type :user/over6
:pred #(and (int? %) (> % 6))
:type-properties {:error/message "should be over 6"
:decode/string mt/-string->long
:json-schema/type "integer"
:json-schema/format "int64"
:json-schema/minimum 6
:gen/gen (gen/large-integer* {:min 7})}}))
(m/into-schema? Over6)
; => true
m/IntoSchema
can be both used as Schema (creating a Schema instance with nil
properties
and children) and as Schema type to create new Schema instances without needing to
register the types:
(m/schema? (m/schema Over6))
; => true
(m/schema? (m/schema [Over6 {:title "over 6"}]))
; => true
:pred
is used for validation:
(m/validate Over6 2)
; => false
(m/validate Over6 7)
; => true
:type-properties
are shared for all schema instances and are used just like Schema
(instance) properties by many Schema applications, including error messages,
value generation and json-schema transformations.
(json-schema/transform Over6)
; => {:type "integer", :format "int64", :minimum 6}
(json-schema/transform [Over6 {:json-schema/example 42}])
; => {:type "integer", :format "int64", :minimum 6, :example 42}
Content dependent simple schema
You can also build content-dependent schemas by using a callback function :compile
of type properties children options -> opts
:
(def Between
(m/-simple-schema
{:type `Between
:compile (fn [_properties [min max] _options]
(when-not (and (int? min) (int? max))
(m/-fail! ::invalid-children {:min min, :max max}))
{:pred #(and (int? %) (>= min % max))
:min 2 ;; at least 1 child
:max 2 ;; at most 1 child
:type-properties {:error/fn (fn [error _] (str "should be between " min " and " max ", was " (:value error)))
:decode/string mt/-string->long
:json-schema {:type "integer"
:format "int64"
:minimum min
:maximum max}
:gen/gen (gen/large-integer* {:min (inc min), :max max})}})}))
(m/form [Between 10 20])
; => [user/Between 10 20]
(-> [Between 10 20]
(m/explain 8)
(me/humanize))
; => ["should be between 10 and 20, was 8"]
(mg/sample [Between -10 10])
; => (-1 0 -2 -4 -4 0 -2 7 1 0)
Schema registry
Schemas are looked up using a malli.registry/Registry
protocol, which is effectively a map from schema type
to a schema recipe (Schema
, IntoSchema
or vector-syntax schema). Map
s can also be used as a registry.
Custom Registry
can be passed into all/most malli public APIs via the optional options map using :registry
key. If omitted, malli.core/default-registry
is used.
;; the default registry
(m/validate [:maybe string?] "kikka")
; => true
;; registry as explicit options
(m/validate [:maybe string?] "kikka" {:registry m/default-registry})
; => true
The default immutable registry is merged from multiple parts, enabling easy re-composition of custom schema sets. See built-in schemas for list of all Schemas.
Custom registry
Here's an example to create a custom registry without the default core predicates and with :neg-int
and :pos-int
Schemas:
(def registry
(merge
(m/class-schemas)
(m/comparator-schemas)
(m/base-schemas)
{:neg-int (m/-simple-schema {:type :neg-int, :pred neg-int?})
:pos-int (m/-simple-schema {:type :pos-int, :pred pos-int?})}))
(m/validate [:or :pos-int :neg-int] 'kikka {:registry registry})
; => false
(m/validate [:or :pos-int :neg-int] 123 {:registry registry})
; => true
We did not register normal predicate schemas:
(m/validate pos-int? 123 {:registry registry})
; Syntax error (ExceptionInfo) compiling
; :malli.core/invalid-schema {:schema pos-int?}
Local registry
Any schema can define a local registry using :registry
schema property:
(def Adult
[:map {:registry {::age [:and int? [:> 18]]}}
[:age ::age]])
(mg/generate Adult {:size 10, :seed 1})
; => {:age 92}
Local registries can be persisted:
(-> Adult
(malli.edn/write-string)
(malli.edn/read-string)
(m/validate {:age 46}))
; => true
See also Recursive Schemas.
Changing the default registry
Passing in custom options to all public methods is a lot of boilerplate. For the lazy, there is an easier way - we can swap the (global) default registry:
(require '[malli.registry :as mr])
;; the default registry
(-> m/default-registry (mr/schemas) (count))
;=> 140
;; global side-effects! free since 0.7.0!
(mr/set-default-registry!
{:string (m/-string-schema)
:maybe (m/-maybe-schema)
:map (m/-map-schema)})
(-> m/default-registry (mr/schemas) (count))
; => 3
(m/validate
[:map [:maybe [:maybe :string]]]
{:maybe "sheep"})
; => true
(m/validate :int 42)
; =throws=> :malli.core/invalid-schema {:schema :int}
NOTE: mr/set-default-registry!
is an imperative api with global side-effects. Easy, but not simple. If you want to disable the api, you can define the following compiler/jvm bootstrap:
- cljs:
:closure-defines {malli.registry/mode "strict"}
- clj:
:jvm-opts ["-Dmalli.registry/mode=strict"]
DCE and schemas
The default schema registry is defined as a Var, so all Schema implementation (100+) are dragged in. For ClojureScript, this means the schemas implementations are not removed via Dead Code Elimination (DCE), resulting a large (37KB, zipped) js-bundle.
Malli allows the default registry to initialized with empty schemas, using the following compiler/jvm bootstrap:
- cljs:
:closure-defines {malli.registry/type "custom"}
- clj:
:jvm-opts ["-Dmalli.registry/type=custom"]
;; with the flag set on
(-> m/default-registry (mr/schemas) (count))
; => 0
With this, you can register just what you need and rest are DCE'd. The previous example results in just a 3KB gzip bundle.
Registry implementations
Malli supports multiple type of registries.
Immutable registry
Just a Map
.
(require '[malli.registry :as mr])
(mr/set-default-registry!
{:string (m/-string-schema)
:maybe (m/-maybe-schema)
:map (m/-map-schema)})
(m/validate
[:map [:maybe [:maybe :string]]]
{:maybe "sheep"})
; => true
Var registry
Var is a valid reference type in Malli. To support auto-resolving Var references to Vars, mr/var-registry
is needed. It is enabled by default.
(def UserId :string)
(def User
[:map
[:id #'UserId]
[:friends {:optional true} [:set [:ref #'User]]]])
(mg/sample User {:seed 0})
;({:id ""}
; {:id "6", :friends #{{:id ""}}}
; {:id ""}
; {:id "4", :friends #{}}
; {:id "24b7"}
; {:id "Uo"}
; {:id "8"}
; {:id "z5b"}
; {:id "R9f"}
; {:id "lUm6Wj9gR"})
Mutable registry
clojure.spec introduces a mutable global registry for specs. The mutable registry in malli forces you to bring in your own state atom and functions how to work with it:
Using a custom registry atom:
(def registry*
(atom {:string (m/-string-schema)
:maybe (m/-maybe-schema)
:map (m/-map-schema)}))
(defn register! [type ?schema]
(swap! registry* assoc type ?schema))
(mr/set-default-registry!
(mr/mutable-registry registry*))
(register! :non-empty-string [:string {:min 1}])
(m/validate :non-empty-string "malli")
; => true
The mutable registry can also be passed in as an explicit option:
(def registry (mr/mutable-registry registry*))
(m/validate :non-empty-string "malli" {:registry registry})
; => true
Dynamic registry
If you know what you are doing, you can also use dynamic scope to pass in default schema registry:
(mr/set-default-registry!
(mr/dynamic-registry))
(binding [mr/*registry* {:string (m/-string-schema)
:maybe (m/-maybe-schema)
:map (m/-map-schema)
:non-empty-string [:string {:min 1}]}]
(m/validate :non-empty-string "malli"))
; => true
Lazy registries
You can provide schemas at runtime using mr/lazy-registry
- it takes a local registry and a provider function of type registry -> schema
as arguments:
(def registry
(mr/lazy-registry
(m/default-schemas)
(fn [type registry]
;; simulates pulling CloudFormation Schemas when needed
(let [lookup {"AWS::ApiGateway::UsagePlan" [:map {:closed true}
[:Type [:= "AWS::ApiGateway::UsagePlan"]]
[:Description {:optional true} string?]
[:UsagePlanName {:optional true} string?]]
"AWS::AppSync::ApiKey" [:map {:closed true}
[:Type [:= "AWS::AppSync::ApiKey"]]
[:ApiId string?]
[:Description {:optional true} string?]]}]
(println "... loaded" type)
(some-> type lookup (m/schema {:registry registry}))))))
;; lazy multi, doesn't realize the schemas
(def CloudFormation
(m/schema
[:multi {:dispatch :Type, :lazy-refs true}
"AWS::ApiGateway::UsagePlan"
"AWS::AppSync::ApiKey"]
{:registry registry}))
(m/validate
CloudFormation
{:Type "AWS::ApiGateway::UsagePlan"
:Description "laiskanlinna"})
; ... loaded AWS::ApiGateway::UsagePlan
; => true
(m/validate
CloudFormation
{:Type "AWS::ApiGateway::UsagePlan"
:Description "laiskanlinna"})
; => true
Composite registry
Registries can be composed, a full example:
(require '[malli.core :as m])
(require '[malli.registry :as mr])
(def registry (atom {}))
(defn register! [type schema]
(swap! registry assoc type schema))
(mr/set-default-registry!
;; linear search
(mr/composite-registry
;; immutable registry
{:map (m/-map-schema)}
;; mutable (spec-like) registry
(mr/mutable-registry registry)
;; on the perils of dynamic scope
(mr/dynamic-registry)))
;; mutate like a boss
(register! :maybe (m/-maybe-schema))
;; ☆.。.:*・°☆.。.:*・°☆.。.:*・°☆.。.:*・°☆
(binding [mr/*registry* {:string (m/-string-schema)}]
(m/validate
[:map [:maybe [:maybe :string]]]
{:maybe "sheep"}))
; => true
Function schemas
Instrumentation
See Instrumentation.
Clj-kondo
Clj-kondo is a linter for Clojure code that sparks joy.
Given functions and function Schemas:
(defn square [x] (* x x))
(m/=> square [:=> [:cat int?] nat-int?])
(defn plus
([x] x)
([x y] (+ x y)))
(m/=> plus [:function
[:=> [:cat int?] int?]
[:=> [:cat int? int?] int?]])
Generating clj-kondo
configuration from current namespace:
(require '[malli.clj-kondo :as mc])
(-> (mc/collect *ns*) (mc/linter-config))
;{:lint-as #:malli.schema{defn schema.core/defn},
; :linters
; {:type-mismatch
; {:namespaces
; {user {square {:arities {1 {:args [:int]
; :ret :pos-int}}}
; plus {:arities {1 {:args [:int]
; :ret :int},
; 2 {:args [:int :int]
; :ret :int}}}}}}}}
Emitting confing into ./.clj-kondo/configs/malli/config.edn
:
(mc/emit!)
In action:
Static type checking via Typed Clojure
Typed Clojure is an optional type system for Clojure.
typed.malli can consume a subset of malli schema syntax to statically type check and infer Clojure code.
See this in action in the malli-type-providers example project.
(ns typed-example.malli-type-providers
(:require [typed.clojure :as t]
[malli.core :as m]))
;; just use malli instrumentation normally
(m/=> foo [:=> [:cat :int] :int])
;; Typed Clojure will statically check `foo` against its schema (after converting it to a type)
(defn foo [t] (inc t))
;; Typed Clojure will automatically infer `foo`s type from its schema
(foo 1)
(comment (t/check-ns-clj)) ;; check this ns
Visualizing schemas
DOT
Transforming Schemas into DOT Language:
(require '[malli.dot :as md])
(def Address
[:schema
{:registry {"Country" [:map
[:name [:enum :FI :PO]]
[:neighbors [:vector [:ref "Country"]]]]
"Burger" [:map
[:name string?]
[:description {:optional true} string?]
[:origin [:maybe "Country"]]
[:price pos-int?]]
"OrderLine" [:map
[:burger "Burger"]
[:amount int?]]
"Order" [:map
[:lines [:vector "OrderLine"]]
[:delivery [:map
[:delivered boolean?]
[:address [:map
[:street string?]
[:zip int?]
[:country "Country"]]]]]]}}
"Order"])
(md/transform Address)
; "digraph { ... }"
Visualized with Graphviz:
PlantUML
Transforming Schemas into PlantUML:
(require '[malli.plantuml :as plantuml])
(plantuml/transform Address)
; "@startuml ... @enduml"
Visualized with PlantText:
Lite
Simple syntax sugar, like data-specs, but for malli.
As the namespace suggests, it's experimental, built for reitit.
(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?})}})
;[: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]]
Options can be used by binding a dynamic l/*options*
Var:
(binding [l/*options* {:registry (merge
(m/default-schemas)
{:user/id :int})}]
(l/schema {:id (l/maybe :user/id)
:child {:id :user/id}}))
;[:map
; [:id [:maybe :user/id]]
; [:child [:map [:id :user/id]]]]
Performance
Malli tries to be really, really fast.
Validation performance
Usually as fast (or faster) as idiomatic Clojure.
(require '[criterium.core :as cc])
(def valid {:x true, :y 1, :z "zorro"})
;; idomatic clojure (54ns)
(let [valid? (fn [{:keys [x y z]}]
(and (boolean? x)
(if y (int? y) true)
(string? z)))]
(assert (valid? valid))
(cc/quick-bench (valid? valid)))
(require '[malli.core :as m])
;; malli (39ns)
(let [valid? (m/validator
[:map
[:x :boolean]
[:y {:optional true} :int]
[:z :string]])]
(assert (valid? valid))
(cc/quick-bench (valid? valid)))
Same with Clojure Spec and Plumatic Schema:
(require '[clojure.spec.alpha :as spec])
(require '[schema.core :as schema])
(spec/def ::x boolean?)
(spec/def ::y int?)
(spec/def ::z string?)
;; clojure.spec (450ns)
(let [spec (spec/keys :req-un [::x ::z] :opt-un [::y])]
(assert (spec/valid? spec valid))
(cc/quick-bench (spec/valid? spec valid)))
;; plumatic schema (660ns)
(let [valid? (schema/checker
{:x schema/Bool
(schema/optional-key :y) schema/Int
:z schema/Str})]
(assert (not (valid? valid)))
(cc/quick-bench (valid? valid)))
Transformation performance
Usually faster than idiomatic Clojure.
(def data {:x "true", :y "1", :z "kikka"})
(def expected {:x true, :y 1, :z "kikka"})
;; idiomatic clojure (290ns)
(let [transform (fn [{:keys [x y] :as m}]
(cond-> m
(string? x) (update :x #(Boolean/parseBoolean %))
(string? y) (update :y #(Long/parseLong %))))]
(assert (= expected (transform data)))
(cc/quick-bench (transform data)))
;; malli (72ns)
(let [schema [:map
[:x :boolean]
[:y {:optional true} int?]
[:z string?]]
transform (m/decoder schema (mt/string-transformer))]
(assert (= expected (transform data)))
(cc/quick-bench (transform data)))
Same with Clojure Spec and Plumatic Schema:
(require '[spec-tools.core :as st])
(require '[schema.coerce :as sc])
(spec/def ::x boolean?)
(spec/def ::y int?)
(spec/def ::z string?)
;; clojure.spec (19000ns)
(let [spec (spec/keys :req-un [::x ::z] :opt-un [::y])
transform #(st/coerce spec % st/string-transformer)]
(assert (= expected (transform data)))
(cc/quick-bench (transform data)))
;; plumatic schema (2200ns)
(let [schema {:x schema/Bool
(schema/optional-key :y) schema/Int
:z schema/Str}
transform (sc/coercer schema sc/string-coercion-matcher)]
(assert (= expected (transform data)))
(cc/quick-bench (transform data)))
The transformation engine is smart enough to just transform parts of the schema that need to be transformed. If there is nothing to transform, identity
function is returned.
(def json->user
(m/decoder
[:map
[:id :int]
[:name :string]
[:address [:map
[:street :string]
[:rural :boolean]
[:country [:enum "finland" "poland"]]]]]
(mt/json-transformer)))
(= identity json->user)
; => true
;; 5ns
(cc/quick-bench
(json->user
{:id 1
:name "tiina"
:address {:street "kotikatu"
:rural true
:country "poland"}}))
Parsing performance
;; 37µs
(let [spec (s/* (s/cat :prop string?,
:val (s/alt :s string?
:b boolean?)))
parse (partial s/conform spec)]
(cc/quick-bench
(parse ["-server" "foo" "-verbose" "-verbose" "-user" "joe"])))
;; 2.4µs
(let [schema [:* [:catn
[:prop string?]
[:val [:altn
[:s string?]
[:b boolean?]]]]]
parse (m/parser schema)]
(cc/quick-bench
(parse ["-server" "foo" "-verbose" "-verbose" "-user" "joe"])))
Built-in schemas
malli.core/predicate-schemas
Contains both function values and unqualified symbol representations for all relevant core predicates. Having both representations enables reading forms from both code (function values) and EDN-files (symbols): any?
, some?
, number?
, integer?
, int?
, pos-int?
, neg-int?
, nat-int?
, pos?
, neg?
, float?
, double?
, boolean?
, string?
, ident?
, simple-ident?
, qualified-ident?
, keyword?
, simple-keyword?
, qualified-keyword?
, symbol?
, simple-symbol?
, qualified-symbol?
, uuid?
, uri?
, decimal?
, inst?
, seqable?
, indexed?
, map?
, vector?
, list?
, seq?
, char?
, set?
, nil?
, false?
, true?
, zero?
, rational?
, coll?
, empty?
, associative?
, sequential?
, ratio?
, bytes?
, ifn?
and fn?
.
malli.core/class-schemas
Class-based schemas, contains java.util.regex.Pattern
& js/RegExp
.
malli.core/comparator-schemas
Comparator functions as keywords: :>
, :>=
, :<
, :<=
, :=
and :not=
.
malli.core/type-schemas
Type-like schemas: :any
, :some
, :nil
, :string
, :int
, :double
, :boolean
, :keyword
, :qualified-keyword
, :symbol
, :qualified-symbol
, and :uuid
.
malli.core/sequence-schemas
Sequence/regex-schemas: :+
, :*
, :?
, :repeat
, :cat
, :alt
, :catn
, :altn
.
malli.core/base-schemas
Contains :and
, :or
, :orn
, :not
, :map
, :map-of
, :vector
, :sequential
, :set
, :enum
, :maybe
, :tuple
, :multi
, :re
, :fn
, :ref
, :=>
, :->
, :function
and :schema
.
malli.util/schemas
:merge
, :union
and :select-keys
.
malli.experimental.time
The time
namespace adds support for time formats as defined by ISO 8601 - Date and time — Representations for information interchange.
Currently supported platform and providing implementations:
- JVM: via the java.time package.
- JS: via the js-joda package
The following schemas and their respective types are provided:
Schema | Example | JVM/js-joda Type (java.time ) |
---|---|---|
:time/duration |
PT0.01S | Duration |
:time/period |
P-1Y100D | Period |
:time/instant |
2022-12-18T12:00:25.840823567Z | Instant |
:time/local-date |
2020-01-01 | LocalDate |
:time/local-date-time |
2020-01-01T12:00:00 | LocalDateTime |
:time/local-time |
12:00:00 | LocalTime |
:time/offset-date-time |
2022-12-18T06:00:25.840823567-06:00 | OffsetDateTime |
:time/offset-time |
12:00:00+00:00 | OffsetTime |
:time/zone-id |
UTC | ZoneId |
:time/zone-offset |
+15:00 | ZoneOffset |
:time/zoned-date-time |
2022-12-18T06:00:25.840823567-06:00[America/Chicago] | ZonedDateTime |
To use these schemas, add the schemas provided by (malli.experimental.time/schemas)
to your registry.
Using time-schemas to default registry:
(require '[malli.experimental.time :as met])
(mr/set-default-registry!
(mr/composite-registry
(m/default-schemas)
(met/schemas)))
To use these schemas in ClojureScript you will need to install the npm packages @js-joda/core
and @js-joda/timezone
.
npm install @js-joda/core @js-joda/timezone
Because historical timezone data can add ~500kb to your ClojureScript build malli does not require the @js-joda/timezone
package directly. You must require timezone data before requiring the malli.experimental.time
namespace if you want
to make use of zone related time objects.
For example, to include only timezone data for +/- 5 years from the time the library was released, use:
(ns com.my-co.my-app
(:require ["@js-joda/timezone/dist/js-joda-timezone-10-year-range"]))
For more info see:
https://github.com/js-joda/js-joda/tree/main/packages/timezone
min/max
Time schemas respect min/max predicates for their respective types:
(import (java.time LocalTime))
[:time/local-time {:min (LocalTime/parse "12:00:00") :max (LocalTime/parse "13:00:00")}]
Will be valid only for local times between 12:00 and 13:00.
For the comparison of Period
s, units are compared to corresponding units and never between.
For example a Period of 1 year will always compare greater than a period of 13 months; that is, conceptually (< P13M P1Y)
If you want to add further constraints you can transform your Period
s before being used in min
and max
per your use-case
or combine the schema with :and
and :fn
for example.
Transformation - malli.experimental.time.transform
The malli.experimental.time.transform
namespace provides a time-transformer
from string to the correct type.
Formats can be configured by providing a formatter
or a pattern
property
- pattern: should be a string
- formatter: should be a DateTimeFormatter
(require '[malli.experimental.time.transform :as mett])
(as-> "20200101" $
(m/decode [:time/local-date {:pattern "yyyyMMdd"}] $ (mett/time-transformer))
(m/encode [:time/local-date {:pattern "yyyy_MM_dd"}] $ (mett/time-transformer))
(= "2020_01_01" $))
; => true
Generators - malli.experimental.time.generator
Require malli.experimental.time.generator
to add support for time schema generators.
Generated data also respects min/max properties.
When generating Period
s there is no way distinguish between nil
values and zero for each unit, so zero units will
not constrain the generator, if you need some of the units to be zero in generated Period
s you can always gen/fmap
the data:
[:time/period {:gen/fmap #(. % withMonths 0) :min (. Period of -10 0 1)}]
This would generate Period
s with a minimum years unit of -10, minimum days unit of 1 and months unit always equal to zero.
Without the fmap the months unit could be any negative or positive integer.
JSON Schema - malli.experimental.time.json-schema
Require malli.experimental.time.json-schema
to add support for json
schema time formats.
Json schema formats map to the following string formats:
- time/local-date: date
- time/offset-time: time
- time/offset-date-time: date-time
- time/duration: duration
Description
You can call describe on a schema to get its description in english:
(require '[malli.experimental.describe :as med])
(med/describe [:map {:closed true}
[:x {:optional true} int?]
[:y :boolean]])
;; => "map where {:x (optional) -> <integer>, :y -> <boolean>} with no other keys"
Links (and thanks)
- Schema https://github.com/plumatic/schema
- Clojure.spec https://clojure.org/guides/spec
- Spell-spec https://github.com/bhauman/spell-spec
- JSON Schema https://json-schema.org/understanding-json-schema
- Spec-provider: https://github.com/stathissideris/spec-provider
- F# Type Providers: https://docs.microsoft.com/en-us/dotnet/fsharp/tutorials/type-providers/
- Minimallist https://github.com/green-coder/minimallist
- malli-instrument https://github.com/setzer22/malli-instrument
- Core.typed https://github.com/clojure/core.typed
- TypeScript https://www.typescriptlang.org/
- Struct https://funcool.github.io/struct/latest/
- Seqexp https://github.com/cgrand/seqexp
- yup https://github.com/jquense/yup
- JOI https://github.com/hapijs/joi
Alpha
The public API of Malli has been quite stable already in pre-alpha and in alpha, we try not to break things. Still, the library is evolving and things like value destructuring could affect public APIs and most likely affect the library extenders, e.g. need to implement a new protocol method for custom schemas.
All changes (breaking or not) will be documented in the CHANGELOG and there will be a migration guide and path if needed.
The API layers and stability:
- public API: public vars, name doesn't start with
-
, e.g.malli.core/validate
. The most stable part of the library, should not change (much) in alpha - extender API: public vars, name starts with
-
, e.g.malli.core/-collection-schema
. Not needed with basic use cases, might evolve during the alpha, follow CHANGELOG for details - experimental: stuff in
malli.experimental
ns, code might change be moved under a separate support library, but you can always copy the old implementation to your project, so ok to use. - private API: private vars and
malli.impl
namespaces, all bets are off.
Development
Malli is open for contributions. Before contributing with a PR, please open an issue for it.
Adding new schema types
To add a new schema type, e.g. :float
, you should adding the following:
- schema definition to
malli.core
+ tests - default encoder/decoder mappings into
malli.transform
+ tests - JSON Schema mappings into
malli.json-schema
+ tests - Generators into
malli.generator
+ tests - OPTIONALLY adding inferrers into
malli.provider
+ tests - update
README.md
Running tests
We use Kaocha and cljs-test-runner as a test runners. Before running the tests, you need to install NPM dependencies.
npm install
./bin/kaocha
./bin/node
Installing locally
clj -Mjar
clj -Minstall
Bundle size for cljs
With default registry (37KB+ Gzipped)
# no sci
npx shadow-cljs run shadow.cljs.build-report app /tmp/report.html
# with sci
npx shadow-cljs run shadow.cljs.build-report app-sci /tmp/report.html
# with cherry
npx shadow-cljs run shadow.cljs.build-report app-cherry /tmp/report.html
With minimal registry (2.4KB+ Gzipped)
# no sci
npx shadow-cljs run shadow.cljs.build-report app2 /tmp/report.html
# with sci
npx shadow-cljs run shadow.cljs.build-report app2-sci /tmp/report.html
# with cherry
npx shadow-cljs run shadow.cljs.build-report app2-cherry /tmp/report.html
Formatting the code
clojure-lsp format
clojure-lsp clean-ns
Checking the generated code
npx shadow-cljs release app --pseudo-names
Testing on GraalVM
Without sci (11Mb)
./bin/native-image demo
./demo '[:set :keyword]' '["kikka" "kukka"]'
With sci (18Mb):
./bin/native-image demosci
./demosci '[:fn (fn [x] (and (int? x) (> x 10)))]]' '12'
Babashka
Since version 0.8.9 malli is compatible with babashka, a native, fast starting Clojure interpreter for scripting.
You can add malli to bb.edn
:
{:deps {metosin/malli {:mvn/version "0.9.0"}}}
or directly in a babashka script:
(ns bb-malli
(:require [babashka.deps :as deps]))
(deps/add-deps '{:deps {metosin/malli {:mvn/version "0.9.0"}}})
(require '[malli.core :as malli])
(prn (malli/validate [:map [:a [:int]]] {:a 1}))
(prn (malli/explain [:map [:a [:int]]] {:a "foo"}))
3rd party libraries
- Aave, a code checking tool for Clojure.
- Gungnir, a high level, data driven database library for Clojure data mapping.
- Regal, Royally reified regular expressions
- Reitit, a fast data-driven router for Clojure/Script.
- wasm.cljc - Spec compliant WebAssembly compiler and decompiler
- malli-instrument - Instrumentation for malli mimicking the clojure.spec.alpha API
- Snoop - Function instrumentation using Malli schemas.
- malli-key-relations - Relational schemas about map keys for malli
- malli-cli - Command-line processing
- malapropism - malli-backed configuration library
- muotti - a graph based value transformer library with malli-support
- malli-select - spec2 selection for Malli (for when you only need part of the herd 🐑)
License
Copyright © 2019-2022 Metosin Oy and contributors.
Available under the terms of the Eclipse Public License 2.0, see LICENSE
.
More Resourcesto explore the angular.
mail [email protected] to add your project or resources here 🔥.
- 1Biff | Clojure web framework
https://biffweb.com/
A Clojure web framework for solo developers.
- 2Kamalavelan / column · GitLab
https://gitlab.com/demonshreder/column
Column sits on top of pedestal to support your software
- 3Clojure Tutorials
https://www.youtube.com/channel/UC6yONKYeoE2P3bsahDtsimg/videos:
A collection programming tutorials for Clojure, covering logic programming, transducers, core.async, program optimization, and many more topics. Q) These videos aren't available in my country! A) Videos are also available via Dropbox and Google Drive: (payment options below) same price, billed once a month. Users get access to raw .mp4 files. This is a manual process, so expect a 1-2 day delay, but this method should work better for some users. If you don't mind the Youtube experience, using this site will most likely be more satisfactory Paypal: http://goo.gl/xgTq0j Bitcoin: http://goo.gl/TUk79e Q) Why do you charge for videos? Isn't Youtube free? A) Unfortunately, in order to get any sort of income from Youtube, videos must have a very high view count. Somewhere in the range of 100k views per video. Getting that sort of viewing of tutorials related to programming in any programming language would be hard.
- 4reborg
https://www.youtube.com/channel/UCH0CkLvbv6yEyrUnw9qujpQ/videos:
A weekly screencast about the functions in the Clojure standard library. The screencast is based on the book "Clojure Standard Library, An Annotated Reference" by Manning available at https://www.manning.com/books/clojure-standard-library.
- 5Misophistful
https://www.youtube.com/user/Misophistful/videos:
Screencasts and presentations about programming with Clojure.
- 6ClojureVids
https://www.youtube.com/channel/UCrwwOZ4h2FQhAdTMfjyQfQA/playlists
ClojureVids is dedicated to delivering high-quality Clojure video training for all skill levels.
- 7Experiments in realtime web framework design. Like Meteor, but for Clojure(Script)
https://github.com/venantius/photon
Experiments in realtime web framework design. Like Meteor, but for Clojure(Script) - venantius/photon
- 8{{ mustache }} for Clojure
https://github.com/fhd/clostache
{{ mustache }} for Clojure. Contribute to fhd/clostache development by creating an account on GitHub.
- 9Clojure JSON and JSON SMILE (binary json format) encoding/decoding
https://github.com/dakrone/cheshire
Clojure JSON and JSON SMILE (binary json format) encoding/decoding - dakrone/cheshire
- 10A neural networks library for Clojure
https://github.com/mrdimosthenis/clj-synapses
A neural networks library for Clojure. Contribute to mrdimosthenis/clj-synapses development by creating an account on GitHub.
- 11Clojure(Script) library for declarative data description and validation
https://github.com/plumatic/schema
Clojure(Script) library for declarative data description and validation - plumatic/schema
- 12Simple, high-performance event-driven HTTP client+server for Clojure
https://github.com/http-kit/http-kit
Simple, high-performance event-driven HTTP client+server for Clojure - http-kit/http-kit
- 13The Pedestal Server-side Libraries
https://github.com/pedestal/pedestal
The Pedestal Server-side Libraries. Contribute to pedestal/pedestal development by creating an account on GitHub.
- 14local. mutable. variables.
https://github.com/ztellman/proteus
local. mutable. variables. Contribute to ztellman/proteus development by creating an account on GitHub.
- 15A library for development of single-page full-stack web applications in clj/cljs
https://github.com/fulcrologic/fulcro
A library for development of single-page full-stack web applications in clj/cljs - fulcrologic/fulcro
- 16fireplace.vim: Clojure REPL support
https://github.com/tpope/vim-fireplace
fireplace.vim: Clojure REPL support. Contribute to tpope/vim-fireplace development by creating an account on GitHub.
- 17Home
https://github.com/cgrand/enlive/wiki
a selector-based (à la CSS) templating and transformation system for Clojure - cgrand/enlive
- 18This is the home of O'Reilly's Clojure Cookbook - http://clojure-cookbook.com
https://github.com/clojure-cookbook/clojure-cookbook
This is the home of O'Reilly's Clojure Cookbook - http://clojure-cookbook.com - clojure-cookbook/clojure-cookbook
- 19Graph based visualization tool for re-frame event chains
https://github.com/ertugrulcetin/re-frame-flow
Graph based visualization tool for re-frame event chains - ertugrulcetin/re-frame-flow
- 20Build software better, together
https://github.com/lacuna/bifurcan:
GitHub is where people build software. More than 100 million people use GitHub to discover, fork, and contribute to over 420 million projects.
- 21Daniel Amber
https://www.youtube.com/c/onthecodeagain/videos:
Hey guys, I'm Daniel Amber! I created this channel to be about all aspects of web development. The initial idea was to have a channel that focused on the process of learning web and software development as a beginner, but I want to expand the channel to cover more advanced programming topics in a variety of different languages including JavaScript, PHP and Clojure. If you are a new programmer and are looking for some good tutorials this is a channel for you. Or if you are a seasoned developer looking for a bit of self-improvement this might be up your alley as well! In short, if you are or want to be a web developer or any kind of coder, then this channel was made for people like you :)
- 22Build software better, together
https://github.com/bitemyapp/revise:
GitHub is where people build software. More than 100 million people use GitHub to discover, fork, and contribute to over 420 million projects.
- 23Moved to Codeberg; this is a convenience mirror
https://github.com/technomancy/leiningen
Moved to Codeberg; this is a convenience mirror. Contribute to technomancy/leiningen development by creating an account on GitHub.
- 24Build software better, together
https://github.com/noprompt/meander:
GitHub is where people build software. More than 100 million people use GitHub to discover, fork, and contribute to over 420 million projects.
- 25Build software better, together
https://github.com/redplanetlabs/specter:
GitHub is where people build software. More than 100 million people use GitHub to discover, fork, and contribute to over 420 million projects.
- 26DI is a dependency injection framework that allows you to define dependencies as cheaply as defining function arguments.
https://github.com/darkleaf/di
DI is a dependency injection framework that allows you to define dependencies as cheaply as defining function arguments. - darkleaf/di
- 27Build software better, together
https://github.com/juxt/tick:
GitHub is where people build software. More than 100 million people use GitHub to discover, fork, and contribute to over 420 million projects.
- 28:rainbow: Simpler Rainbow Parentheses
https://github.com/junegunn/rainbow_parentheses.vim
:rainbow: Simpler Rainbow Parentheses. Contribute to junegunn/rainbow_parentheses.vim development by creating an account on GitHub.
- 29An optimized pattern matching library for Clojure
https://github.com/clojure/core.match
An optimized pattern matching library for Clojure. Contribute to clojure/core.match development by creating an account on GitHub.
- 30a catalog of common Clojure errors and their meaning
https://github.com/yogthos/clojure-error-message-catalog
a catalog of common Clojure errors and their meaning - yogthos/clojure-error-message-catalog
- 31Security library for Clojure
https://github.com/funcool/buddy
Security library for Clojure. Contribute to funcool/buddy development by creating an account on GitHub.
- 32Build software better, together
https://github.com/jimpil/duratom:
GitHub is where people build software. More than 100 million people use GitHub to discover, fork, and contribute to over 420 million projects.
- 33Build software better, together
https://github.com/apa512/clj-rethinkdb:
GitHub is where people build software. More than 100 million people use GitHub to discover, fork, and contribute to over 420 million projects.
- 34Modeling domain data on the basis of Clojure records.
https://github.com/friemen/domaintypes
Modeling domain data on the basis of Clojure records. - friemen/domaintypes
- 35A Clojure linter focused on style and code shape.
https://github.com/NoahTheDuke/splint
A Clojure linter focused on style and code shape. Contribute to NoahTheDuke/splint development by creating an account on GitHub.
- 36First-class patterns for Clojure. Made with love, functions, and just the right amount of syntax.
https://github.com/missingfaktor/akar
First-class patterns for Clojure. Made with love, functions, and just the right amount of syntax. - missingfaktor/akar
- 37A document database written in Clojure
https://github.com/robashton/cravendb
A document database written in Clojure. Contribute to robashton/cravendb development by creating an account on GitHub.
- 38Project your Clojure(Script) REPL into the same context as your code when it ran
https://github.com/vvvvalvalval/scope-capture
Project your Clojure(Script) REPL into the same context as your code when it ran - vvvvalvalval/scope-capture
- 39Clojure bindings for the BigML.io API
https://github.com/bigmlcom/clj-bigml
Clojure bindings for the BigML.io API. Contribute to bigmlcom/clj-bigml development by creating an account on GitHub.
- 40Easier-than-print dataflow tracing to tap> and Portal with automatic last-input function replay on eval, instant re-render and effortless extraction of traced data
https://github.com/gnl/playback
Easier-than-print dataflow tracing to tap> and Portal with automatic last-input function replay on eval, instant re-render and effortless extraction of traced data - gnl/playback
- 41Multi-target Clojure/script HTTP client
https://github.com/nervous-systems/kvlt
Multi-target Clojure/script HTTP client. Contribute to nervous-systems/kvlt development by creating an account on GitHub.
- 42Clojure wrappers for Lettuce (Java Redis client)
https://github.com/lerouxrgd/celtuce
Clojure wrappers for Lettuce (Java Redis client) . Contribute to lerouxrgd/celtuce development by creating an account on GitHub.
- 43A small spying and stubbing library for Clojure and ClojureScript
https://github.com/GreenPowerMonitor/test-doubles
A small spying and stubbing library for Clojure and ClojureScript - GreenPowerMonitor/test-doubles
- 44Clojure support for Visual Studio Code
https://github.com/avli/clojureVSCode
Clojure support for Visual Studio Code. Contribute to avli/clojureVSCode development by creating an account on GitHub.
- 45An extensible authentication and authorization library for Clojure Ring web applications and services.
https://github.com/cemerick/friend
An extensible authentication and authorization library for Clojure Ring web applications and services. - GitHub - cemerick/friend: An extensible authentication and authorization library for Clojur...
- 46A migration library for clojure
https://github.com/macourtney/drift
A migration library for clojure. Contribute to macourtney/drift development by creating an account on GitHub.
- 47Clojure library for fast JSON encoding and decoding.
https://github.com/metosin/jsonista
Clojure library for fast JSON encoding and decoding. - metosin/jsonista
- 48Fibers, Channels and Actors for Clojure
https://github.com/puniverse/pulsar
Fibers, Channels and Actors for Clojure. Contribute to puniverse/pulsar development by creating an account on GitHub.
- 49A Clojure library for JavaFX
https://github.com/aaronc/fx-clj
A Clojure library for JavaFX. Contribute to aaronc/fx-clj development by creating an account on GitHub.
- 50Clojure wrapper around JGit
https://github.com/clj-jgit/clj-jgit
Clojure wrapper around JGit. Contribute to clj-jgit/clj-jgit development by creating an account on GitHub.
- 51Clojure REPL that is aware of surrounding lexical scope
https://github.com/GeorgeJahad/debug-repl
Clojure REPL that is aware of surrounding lexical scope - GeorgeJahad/debug-repl
- 52A better Vim integration story for Clojure
https://github.com/dgrnbrg/vim-redl
A better Vim integration story for Clojure. Contribute to dgrnbrg/vim-redl development by creating an account on GitHub.
- 53Verbal-Exprejon is a Clojure library that helps you build complex regexes without any regex
https://github.com/WeshGuillaume/Verbal-Exprejon
Verbal-Exprejon is a Clojure library that helps you build complex regexes without any regex - WeshGuillaume/Verbal-Exprejon
- 54(+ clj cljs datomic datascript re-frame-esque-frp)
https://github.com/metasoarous/datsys
(+ clj cljs datomic datascript re-frame-esque-frp) - metasoarous/datsys
- 55JDBC library for Clojure
https://github.com/funcool/clojure.jdbc
JDBC library for Clojure. Contribute to funcool/clojure.jdbc development by creating an account on GitHub.
- 56Build software better, together
https://github.com/Malabarba/lazy-map-clojure:
GitHub is where people build software. More than 100 million people use GitHub to discover, fork, and contribute to over 420 million projects.
- 57Build tooling for Clojure.
https://github.com/boot-clj/boot
Build tooling for Clojure. Contribute to boot-clj/boot development by creating an account on GitHub.
- 58Build software better, together
https://github.com/datacrypt-project/hitchhiker-tree:
GitHub is where people build software. More than 100 million people use GitHub to discover, fork, and contribute to over 420 million projects.
- 59Build software better, together
https://github.com/mpenet/spandex:
GitHub is where people build software. More than 100 million people use GitHub to discover, fork, and contribute to over 420 million projects.
- 60Build software better, together
https://github.com/amalloy/ordered:
GitHub is where people build software. More than 100 million people use GitHub to discover, fork, and contribute to over 420 million projects.
- 61Build software better, together
https://github.com/AppsFlyer/aerospike-clj:
GitHub is where people build software. More than 100 million people use GitHub to discover, fork, and contribute to over 420 million projects.
- 62Build software better, together
https://github.com/mpenet/alia:
GitHub is where people build software. More than 100 million people use GitHub to discover, fork, and contribute to over 420 million projects.
- 63Build software better, together
https://github.com/TheLadders/sqlium/
GitHub is where people build software. More than 100 million people use GitHub to discover, fork, and contribute to over 420 million projects.
- 64Build software better, together
https://github.com/xtdb/xtdb:
GitHub is where people build software. More than 100 million people use GitHub to discover, fork, and contribute to over 420 million projects.
- 65Clojure HTTP server abstraction
https://github.com/ring-clojure/ring
Clojure HTTP server abstraction. Contribute to ring-clojure/ring development by creating an account on GitHub.
- 66Build software better, together
https://github.com/Factual/durable-queue:
GitHub is where people build software. More than 100 million people use GitHub to discover, fork, and contribute to over 420 million projects.
- 67Fast library for rendering HTML in Clojure
https://github.com/weavejester/hiccup
Fast library for rendering HTML in Clojure. Contribute to weavejester/hiccup development by creating an account on GitHub.
- 68A Library to parse natural language in pure Clojure and ClojureScript
https://github.com/turbopape/postagga
A Library to parse natural language in pure Clojure and ClojureScript - turbopape/postagga
- 69JDBC from Clojure (formerly clojure.contrib.sql)
https://github.com/clojure/java.jdbc
JDBC from Clojure (formerly clojure.contrib.sql). Contribute to clojure/java.jdbc development by creating an account on GitHub.
- 70A concise routing library for Ring/Clojure
https://github.com/weavejester/compojure
A concise routing library for Ring/Clojure. Contribute to weavejester/compojure development by creating an account on GitHub.
- 71A Clojure(script) SQL library for building APIs: Datomic® (GraphQL-ish) pull syntax, data driven configuration, dynamic filtering with relations in mind
https://github.com/walkable-server/walkable
A Clojure(script) SQL library for building APIs: Datomic® (GraphQL-ish) pull syntax, data driven configuration, dynamic filtering with relations in mind - walkable-server/walkable
- 72A library to create and manipulate SQL database schemas with migrations support.
https://github.com/budu/lobos
A library to create and manipulate SQL database schemas with migrations support. - budu/lobos
- 73Database-independent migration library
https://github.com/weavejester/ragtime
Database-independent migration library. Contribute to weavejester/ragtime development by creating an account on GitHub.
- 74Reloaded components à la carte
https://github.com/danielsz/system
Reloaded components à la carte. Contribute to danielsz/system development by creating an account on GitHub.
- 75Clojure practice challenges – train on code kata
https://www.codewars.com/kata/search/clojure
Practice Clojure coding with code challenges designed to engage your programming skills. Solve coding problems and pick up new techniques from your fellow peers.
- 76A simple, fast and versatile Datalog database
https://github.com/juji-io/datalevin
A simple, fast and versatile Datalog database. Contribute to juji-io/datalevin development by creating an account on GitHub.
- 77ClojureScript compilation made easy
https://github.com/thheller/shadow-cljs
ClojureScript compilation made easy. Contribute to thheller/shadow-cljs development by creating an account on GitHub.
- 78A fast data-driven routing library for Clojure/Script
https://github.com/metosin/reitit
A fast data-driven routing library for Clojure/Script - metosin/reitit
- 79Staged compilation for Clojure through environment & special-form aware syntax-quoting.
https://github.com/brandonbloom/metaclj
Staged compilation for Clojure through environment & special-form aware syntax-quoting. - brandonbloom/metaclj
- 80A music programming language for musicians. :notes:
https://github.com/alda-lang/alda
A music programming language for musicians. :notes: - alda-lang/alda
- 81Connection pools for JDBC databases. Simple wrapper around C3P0.
https://github.com/metabase/connection-pool
Connection pools for JDBC databases. Simple wrapper around C3P0. - metabase/connection-pool
- 82Complete instrumentation for clojure.spec
https://github.com/jeaye/orchestra
Complete instrumentation for clojure.spec. Contribute to jeaye/orchestra development by creating an account on GitHub.
- 83Facilities for async programming and communication in Clojure
https://github.com/clojure/core.async
Facilities for async programming and communication in Clojure - clojure/core.async
- 84A library for calendar operations that are aware of weekends and holidays
https://github.com/luciolucio/holi
A library for calendar operations that are aware of weekends and holidays - luciolucio/holi
- 85Category theory concepts in Clojure - Functors, Applicatives, Monads, Monoids and more.
https://github.com/uncomplicate/fluokitten
Category theory concepts in Clojure - Functors, Applicatives, Monads, Monoids and more. - uncomplicate/fluokitten
- 86Klipse is a JavaScript plugin for embedding interactive code snippets in tech blogs.
https://github.com/viebel/klipse
Klipse is a JavaScript plugin for embedding interactive code snippets in tech blogs. - viebel/klipse
- 87Leiningen plugin for consuming and compiling protobuf schemas
https://github.com/AppsFlyer/lein-protodeps
Leiningen plugin for consuming and compiling protobuf schemas - AppsFlyer/lein-protodeps
- 88tools.build's missing piece – install, sign and deploy libraries easily and securely like with Leiningen
https://github.com/gnl/build.simple
tools.build's missing piece – install, sign and deploy libraries easily and securely like with Leiningen - gnl/build.simple
- 89Macros for defining monads, and definition of the most common monads
https://github.com/clojure/algo.monads
Macros for defining monads, and definition of the most common monads - clojure/algo.monads
- 90A Clojure wrapper to HikariCP JDBC connection pool
https://github.com/tomekw/hikari-cp
A Clojure wrapper to HikariCP JDBC connection pool - tomekw/hikari-cp
- 91The Clojure Interactive Development Environment that Rocks for Emacs
https://github.com/clojure-emacs/cider
The Clojure Interactive Development Environment that Rocks for Emacs - clojure-emacs/cider
- 92Midje provides a migration path from clojure.test to a more flexible, readable, abstract, and gracious style of testing
https://github.com/marick/Midje
Midje provides a migration path from clojure.test to a more flexible, readable, abstract, and gracious style of testing - marick/Midje
- 93A date and time library for Clojure, wrapping the Joda Time library.
https://github.com/clj-time/clj-time
A date and time library for Clojure, wrapping the Joda Time library. - clj-time/clj-time
- 94A powerful Clojure web library, full HTTP, full async - see https://juxt.pro/yada/index.html
https://github.com/juxt/yada
A powerful Clojure web library, full HTTP, full async - see https://juxt.pro/yada/index.html - juxt/yada
- 95Asynchronous streaming communication for Clojure - web server, web client, and raw TCP/UDP
https://github.com/clj-commons/aleph
Asynchronous streaming communication for Clojure - web server, web client, and raw TCP/UDP - clj-commons/aleph
- 96An integrated security system for applications built on component
https://github.com/juxt/bolt
An integrated security system for applications built on component - juxt/bolt
- 97Category Theory and Algebraic abstractions for Clojure and ClojureScript.
https://github.com/funcool/cats
Category Theory and Algebraic abstractions for Clojure and ClojureScript. - funcool/cats
- 98A functional effect and streaming system for Clojure/Script
https://github.com/leonoel/missionary
A functional effect and streaming system for Clojure/Script - leonoel/missionary
- 99Coroutine support for clojure
https://github.com/leonoel/cloroutine
Coroutine support for clojure. Contribute to leonoel/cloroutine development by creating an account on GitHub.
- 100Facilities for async programming and communication in Clojure
https://github.com/clojure/core.async/
Facilities for async programming and communication in Clojure - clojure/core.async
- 101Clojure and Clojurescript support for Gradle
https://github.com/clojurephant/clojurephant
Clojure and Clojurescript support for Gradle. Contribute to clojurephant/clojurephant development by creating an account on GitHub.
- 102The Next-Level background job processing library for Clojure
https://github.com/nilenso/goose
The Next-Level background job processing library for Clojure - nilenso/goose
- 103A modern low-level Clojure wrapper for JDBC-based access to databases.
https://github.com/seancorfield/next-jdbc
A modern low-level Clojure wrapper for JDBC-based access to databases. - seancorfield/next-jdbc
- 104A Clojure hierarchical set.
https://github.com/llasram/hier-set
A Clojure hierarchical set. Contribute to llasram/hier-set development by creating an account on GitHub.
- 105Flexible datastore migration and seeding for Clojure projects
https://github.com/juxt/joplin
Flexible datastore migration and seeding for Clojure projects - juxt/joplin
- 106Tools for transparent data transformation
https://github.com/noprompt/meander
Tools for transparent data transformation. Contribute to noprompt/meander development by creating an account on GitHub.
- 107Modern cryptography (libsodium/NaCl) for Clojure
https://github.com/lvh/caesium
Modern cryptography (libsodium/NaCl) for Clojure. Contribute to lvh/caesium development by creating an account on GitHub.
- 108An optional type system for Clojure
https://github.com/clojure/core.typed
An optional type system for Clojure. Contribute to clojure/core.typed development by creating an account on GitHub.
- 109Clojure email support
https://github.com/drewr/postal
Clojure email support. Contribute to drewr/postal development by creating an account on GitHub.
- 110efficient small collections for clojure
https://github.com/ztellman/clj-tuple
efficient small collections for clojure. Contribute to ztellman/clj-tuple development by creating an account on GitHub.
- 111Join clojurians on Slack
http://clojurians.net/
Slack is a new way to communicate with your team. It’s faster, better organized, and more secure than email.
- 112Clojure support for protocol buffers
https://github.com/AppsFlyer/pronto
Clojure support for protocol buffers. Contribute to AppsFlyer/pronto development by creating an account on GitHub.
- 113Tasty SQL for Clojure.
https://github.com/korma/Korma
Tasty SQL for Clojure. Contribute to korma/Korma development by creating an account on GitHub.
- 114Structural validation library for Clojure(Script)
https://github.com/funcool/struct
Structural validation library for Clojure(Script). Contribute to funcool/struct development by creating an account on GitHub.
- 115MIGRATE ALL THE THINGS!
https://github.com/yogthos/migratus
MIGRATE ALL THE THINGS! Contribute to yogthos/migratus development by creating an account on GitHub.
- 116Clojure on Exercism
http://exercism.io/languages/clojure
Get fluent in Clojure by solving 88 exercises. And then level up with mentoring from our world-class team.
- 117A library designed to generate cryptographically strong random numbers suitable for managing data such as passwords, account authentication, security tokens, and related secrets.
https://github.com/lk-geimfari/secrets.clj
A library designed to generate cryptographically strong random numbers suitable for managing data such as passwords, account authentication, security tokens, and related secrets. - lk-geimfari/secr...
- 118A Leiningen plugin designed to tell you your code is bad, and that you should feel bad
https://github.com/dakrone/lein-bikeshed
A Leiningen plugin designed to tell you your code is bad, and that you should feel bad - dakrone/lein-bikeshed
- 119Terminal UI library for Clojure
https://github.com/lambdaisland/trikl
Terminal UI library for Clojure. Contribute to lambdaisland/trikl development by creating an account on GitHub.
- 120Basic REPL breakpoints.
https://github.com/technomancy/limit-break
Basic REPL breakpoints. Contribute to technomancy/limit-break development by creating an account on GitHub.
- 121Multilingual library to easily parse date strings to java.util.Date objects.
https://github.com/tokenmill/timewords
Multilingual library to easily parse date strings to java.util.Date objects. - tokenmill/timewords
- 122Bidirectional Ring router. REST oriented. Rails inspired.
https://github.com/darkleaf/router
Bidirectional Ring router. REST oriented. Rails inspired. - darkleaf/router
- 123A Clojurey wrapper around the Lanterna terminal output library.
https://github.com/MultiMUD/clojure-lanterna
A Clojurey wrapper around the Lanterna terminal output library. - MultiMUD/clojure-lanterna
- 124Emacs minor mode that keeps your code always indented. More reliable than electric-indent-mode.
https://github.com/Malabarba/aggressive-indent-mode
Emacs minor mode that keeps your code always indented. More reliable than electric-indent-mode. - Malabarba/aggressive-indent-mode
- 125Write Java inside Clojure
https://github.com/tailrecursion/javastar
Write Java inside Clojure. Contribute to tailrecursion/javastar development by creating an account on GitHub.
- 126A clojure(script) unit testing framework designed to be used from the REPL
https://github.com/amokfa/datest
A clojure(script) unit testing framework designed to be used from the REPL - amokfa/datest
- 127A macro to define clojure functions with parameter pattern matching just like erlang or elixir.
https://github.com/killme2008/defun
A macro to define clojure functions with parameter pattern matching just like erlang or elixir. - killme2008/defun
- 128Managed lifecycle of stateful objects in Clojure
https://github.com/stuartsierra/component
Managed lifecycle of stateful objects in Clojure. Contribute to stuartsierra/component development by creating an account on GitHub.
- 1291.3 update of clojure.contrib.trace
https://github.com/clojure/tools.trace
1.3 update of clojure.contrib.trace. Contribute to clojure/tools.trace development by creating an account on GitHub.
- 130Small library for using neural networks and core.matrix
https://github.com/gigasquid/k9
Small library for using neural networks and core.matrix - gigasquid/k9
- 131A Vim plugin for Clojure's Eastwood linter
https://github.com/venantius/vim-eastwood
A Vim plugin for Clojure's Eastwood linter. Contribute to venantius/vim-eastwood development by creating an account on GitHub.
- 132Clojure & ClojureScript Language Server (LSP) implementation
https://github.com/clojure-lsp/clojure-lsp
Clojure & ClojureScript Language Server (LSP) implementation - clojure-lsp/clojure-lsp
- 133Application state management made simple: a Clojure map that implements java.io.Closeable.
https://github.com/piotr-yuxuan/closeable-map
Application state management made simple: a Clojure map that implements java.io.Closeable. - piotr-yuxuan/closeable-map
- 134A simple validation library for Clojure and ClojureScript
https://github.com/markwoodhall/clova
A simple validation library for Clojure and ClojureScript - markwoodhall/clova
- 135A classy high-level Clojure library for defining application models and retrieving them from a DB
https://github.com/metabase/toucan
A classy high-level Clojure library for defining application models and retrieving them from a DB - metabase/toucan
- 136Build software better, together
https://github.com/hellonico/origami:
GitHub is where people build software. More than 100 million people use GitHub to discover, fork, and contribute to over 420 million projects.
- 137Fast Clojure Matrix Library
https://github.com/uncomplicate/neanderthal
Fast Clojure Matrix Library. Contribute to uncomplicate/neanderthal development by creating an account on GitHub.
- 138A fast clojure console library
https://github.com/aaron-santos/zaffre
A fast clojure console library. Contribute to kelsey-sorrels/zaffre development by creating an account on GitHub.
- 139Light structure and support for dependency injection
https://github.com/juxt/clip
Light structure and support for dependency injection - juxt/clip
- 140A DSL in Clojure for SQL query, DML, and DDL. Supports a majority of MySQL's statements.
https://github.com/stch-library/sql
A DSL in Clojure for SQL query, DML, and DDL. Supports a majority of MySQL's statements. - stch-library/sql
- 141Enhanced try and throw for Clojure leveraging Clojure's capabilities
https://github.com/scgilardi/slingshot
Enhanced try and throw for Clojure leveraging Clojure's capabilities - scgilardi/slingshot
- 142Clojure wrapper for the Tesseract OCR software
https://github.com/antoniogarrote/clj-tesseract
Clojure wrapper for the Tesseract OCR software. Contribute to antoniogarrote/clj-tesseract development by creating an account on GitHub.
- 143inference and machine learning in clojure
https://github.com/aria42/infer
inference and machine learning in clojure. Contribute to aria42/infer development by creating an account on GitHub.
- 144Automatic PostgreSQL CRUD queries
https://github.com/tatut/specql/
Automatic PostgreSQL CRUD queries. Contribute to tatut/specql development by creating an account on GitHub.
- 145A Leiningen plugin for a superior development environment
https://github.com/venantius/ultra
A Leiningen plugin for a superior development environment - venantius/ultra
- 146SWANK and nREPL servers for clojure providing JPDA based debuggers
https://github.com/pallet/ritz
SWANK and nREPL servers for clojure providing JPDA based debuggers - pallet/ritz
- 147bifurcan/test/bifurcan at master · lacuna/bifurcan
https://github.com/lacuna/bifurcan/blob/master/test/bifurcan
functional, durable data structures. Contribute to lacuna/bifurcan development by creating an account on GitHub.
- 148Trace-oriented debugging tools for Clojure
https://github.com/dgrnbrg/spyscope
Trace-oriented debugging tools for Clojure. Contribute to dgrnbrg/spyscope development by creating an account on GitHub.
- 149A compatibility layer for event-driven abstractions
https://github.com/ztellman/manifold
A compatibility layer for event-driven abstractions - clj-commons/manifold
- 150An HTTP client for Clojure, wrapping JDK 11's HttpClient
https://github.com/gnarroway/hato
An HTTP client for Clojure, wrapping JDK 11's HttpClient - gnarroway/hato
- 151A small machine learning library written in Lisp (Clojure) aimed at providing simple, concise implementations of machine learning techniques and utilities.
https://github.com/cloudkj/lambda-ml
A small machine learning library written in Lisp (Clojure) aimed at providing simple, concise implementations of machine learning techniques and utilities. - cloudkj/lambda-ml
- 152code-walking without caveats
https://github.com/ztellman/riddley
code-walking without caveats. Contribute to ztellman/riddley development by creating an account on GitHub.
- 153A better IDE integration story for Clojure
https://github.com/dgrnbrg/redl
A better IDE integration story for Clojure. Contribute to dgrnbrg/redl development by creating an account on GitHub.
- 154Machine Learning in Clojure
https://github.com/rinuboney/clatern
Machine Learning in Clojure. Contribute to rinuboney/clatern development by creating an account on GitHub.
- 155Sweet web apis with Compojure & Swagger
https://github.com/metosin/compojure-api
Sweet web apis with Compojure & Swagger. Contribute to metosin/compojure-api development by creating an account on GitHub.
- 156Suite of tools for deploying and training deep learning models using the JVM. Highlights include model import for keras, tensorflow, and onnx/pytorch, a modular and tiny c++ library for running math code and a java based math library on top of the core c++ library. Also includes samediff: a pytorch/tensorflow like library for running deep learn...
https://github.com/deeplearning4j/deeplearning4j
Suite of tools for deploying and training deep learning models using the JVM. Highlights include model import for keras, tensorflow, and onnx/pytorch, a modular and tiny c++ library for running mat...
- 157Exception net
https://github.com/mpenet/ex
Exception net. Contribute to mpenet/ex development by creating an account on GitHub.
- 158Minimalistic statistics library for Clojure
https://github.com/clojurewerkz/statistiker
Minimalistic statistics library for Clojure. Contribute to clojurewerkz/statistiker development by creating an account on GitHub.
- 159Generate Graphviz diagrams from FSM data
https://github.com/jebberjeb/fsmviz
Generate Graphviz diagrams from FSM data. Contribute to jebberjeb/fsmviz development by creating an account on GitHub.
- 160A minimalist's unit testing framework ("classic" version)
https://github.com/clojure-expectations/expectations
A minimalist's unit testing framework ("classic" version) - clojure-expectations/expectations
- 161High-performance Bayesian Data Analysis on the GPU in Clojure
https://github.com/uncomplicate/bayadera
High-performance Bayesian Data Analysis on the GPU in Clojure - uncomplicate/bayadera
- 162A Clojure library designed to provide hassle-free, ready to go gRPC experience without ton of preparations and Java code.
https://github.com/otwieracz/clj-grpc
A Clojure library designed to provide hassle-free, ready to go gRPC experience without ton of preparations and Java code. - otwieracz/clj-grpc
- 163Awesome print: like clojure.pprint, but awesome
https://github.com/razum2um/aprint
Awesome print: like clojure.pprint, but awesome. Contribute to razum2um/aprint development by creating an account on GitHub.
- 164Visualize Clojure zippers using Graphviz
https://github.com/lambdaisland/zipper-viz
Visualize Clojure zippers using Graphviz. Contribute to lambdaisland/zipper-viz development by creating an account on GitHub.
- 165A fast, immutable, distributed & compositional Datalog engine for everyone.
https://github.com/replikativ/datahike
A fast, immutable, distributed & compositional Datalog engine for everyone. - GitHub - replikativ/datahike: A fast, immutable, distributed & compositional Datalog engine for everyone.
- 166Clojure-based, R-like statistical computing and graphics environment for the JVM
https://github.com/incanter/incanter
Clojure-based, R-like statistical computing and graphics environment for the JVM - incanter/incanter
- 167simple graph and tree visualization
https://github.com/ztellman/rhizome
simple graph and tree visualization. Contribute to ztellman/rhizome development by creating an account on GitHub.
- 168A Vim plugin for cljfmt, the Clojure formatting tool.
https://github.com/venantius/vim-cljfmt
A Vim plugin for cljfmt, the Clojure formatting tool. - venantius/vim-cljfmt
- 169Micro-framework for data-driven architecture
https://github.com/weavejester/integrant
Micro-framework for data-driven architecture. Contribute to weavejester/integrant development by creating an account on GitHub.
- 170Immutable database and Datalog query engine for Clojure, ClojureScript and JS
https://github.com/tonsky/datascript
Immutable database and Datalog query engine for Clojure, ClojureScript and JS - tonsky/datascript
- 171Clojure wrapper for Encog (v3) (Machine-Learning framework that specialises in neural-nets)
https://github.com/jimpil/enclog
Clojure wrapper for Encog (v3) (Machine-Learning framework that specialises in neural-nets) - jimpil/enclog
- 172A Parser Combinators Library for Clojure
https://github.com/blancas/kern
A Parser Combinators Library for Clojure. Contribute to blancas/kern development by creating an account on GitHub.
- 173An idiomatic clojure http client wrapping the apache client. Officially supported version.
https://github.com/dakrone/clj-http
An idiomatic clojure http client wrapping the apache client. Officially supported version. - dakrone/clj-http
- 174Utility library for writing microservices in Clojure, with support for Swagger and OAuth
https://github.com/zalando/friboo
Utility library for writing microservices in Clojure, with support for Swagger and OAuth - zalando/friboo
- 175A Clojure & ClojureScript DSL for SQL
https://github.com/r0man/sqlingvo
A Clojure & ClojureScript DSL for SQL. Contribute to r0man/sqlingvo development by creating an account on GitHub.
- 176Generate images from Graphviz dot strings in Clojure and Clojurescript
https://github.com/jebberjeb/viz.cljc
Generate images from Graphviz dot strings in Clojure and Clojurescript - jebberjeb/viz.cljc
- 177ClojureCL is a Clojure library for parallel computations with OpenCL.
https://github.com/uncomplicate/clojurecl
ClojureCL is a Clojure library for parallel computations with OpenCL. - uncomplicate/clojurecl
- 178arohner/spectrum
https://github.com/arohner/spectrum
Contribute to arohner/spectrum development by creating an account on GitHub.
- 179A machine learning library for Clojure built on top of Weka and friends
https://github.com/antoniogarrote/clj-ml
A machine learning library for Clojure built on top of Weka and friends - antoniogarrote/clj-ml
- 180Clojure library for debugging core functions
https://github.com/AppsFlyer/mate-clj
Clojure library for debugging core functions. Contribute to AppsFlyer/mate-clj development by creating an account on GitHub.
- 181A Clojure DSL for Apache Spark
https://github.com/yieldbot/flambo
A Clojure DSL for Apache Spark. Contribute to sorenmacbeth/flambo development by creating an account on GitHub.
- 182An extremely light layer over TensorFlow's Java api
https://github.com/kieranbrowne/clojure-tensorflow
An extremely light layer over TensorFlow's Java api - kieranbrowne/clojure-tensorflow
- 183Neural Networks in Clojure
https://github.com/japonophile/synaptic
Neural Networks in Clojure. Contribute to japonophile/synaptic development by creating an account on GitHub.
- 184Clojure test coverage tool
https://github.com/cloverage/cloverage
Clojure test coverage tool. Contribute to cloverage/cloverage development by creating an account on GitHub.
- 185Interactive evaluation for Neovim (Clojure, Fennel, Janet, Racket, Hy, MIT Scheme, Guile, Python and more!)
https://github.com/Olical/conjure
Interactive evaluation for Neovim (Clojure, Fennel, Janet, Racket, Hy, MIT Scheme, Guile, Python and more!) - Olical/conjure
- 186A nice little wrapper around java.util.zip.* using streams
https://github.com/AeroNotix/swindon
A nice little wrapper around java.util.zip.* using streams - AeroNotix/swindon
- 187A Clojure 3D Game Engine (Wrapper), Powered by jMonkeyEngine
https://github.com/ertugrulcetin/jme-clj
A Clojure 3D Game Engine (Wrapper), Powered by jMonkeyEngine - ertugrulcetin/jme-clj
- 188Clojure documentation tool
https://github.com/weavejester/codox
Clojure documentation tool. Contribute to weavejester/codox development by creating an account on GitHub.
- 189A Leiningen plugin for finding dead code
https://github.com/venantius/yagni
A Leiningen plugin for finding dead code. Contribute to venantius/yagni development by creating an account on GitHub.
- 190Dynamic Tensor Graph library in Clojure (think PyTorch, DynNet, etc.)
https://github.com/aria42/flare
Dynamic Tensor Graph library in Clojure (think PyTorch, DynNet, etc.) - aria42/flare
- 191salve.vim: static support for Leiningen and Boot
https://github.com/tpope/vim-salve
salve.vim: static support for Leiningen and Boot. Contribute to tpope/vim-salve development by creating an account on GitHub.
- 192Clojure Katas inspired by Alice in Wonderland
https://github.com/gigasquid/wonderland-clojure-katas
Clojure Katas inspired by Alice in Wonderland. Contribute to gigasquid/wonderland-clojure-katas development by creating an account on GitHub.
- 193a library to define a continuous delivery pipeline in code
https://github.com/flosell/lambdacd
a library to define a continuous delivery pipeline in code - flosell/lambdacd
- 194Efficient, hassle-free function call validation with a concise inline syntax for clojure.spec and Malli
https://github.com/fulcrologic/guardrails
Efficient, hassle-free function call validation with a concise inline syntax for clojure.spec and Malli - fulcrologic/guardrails
- 195Java 8 Date-Time API for Clojure
https://github.com/dm3/clojure.java-time
Java 8 Date-Time API for Clojure. Contribute to dm3/clojure.java-time development by creating an account on GitHub.
- 196Minor mode for Emacs that deals with parens pairs and tries to be smart about it.
https://github.com/Fuco1/smartparens
Minor mode for Emacs that deals with parens pairs and tries to be smart about it. - Fuco1/smartparens
- 197Clojure library for CUDA development
https://github.com/uncomplicate/clojurecuda
Clojure library for CUDA development. Contribute to uncomplicate/clojurecuda development by creating an account on GitHub.
- 198Utility library for Clojure and ClojureScript
https://github.com/ertugrulcetin/kezban
Utility library for Clojure and ClojureScript. Contribute to ertugrulcetin/kezban development by creating an account on GitHub.
- 199Flexible retries library for Clojure
https://github.com/grammarly/perseverance
Flexible retries library for Clojure. Contribute to grammarly/perseverance development by creating an account on GitHub.
- 200Machine learning in Clojure
https://github.com/originrose/cortex
Machine learning in Clojure. Contribute to originrose/cortex development by creating an account on GitHub.
- 201Integration testing framework using a state monad in the backend for building and composing flows
https://github.com/nubank/state-flow
Integration testing framework using a state monad in the backend for building and composing flows - nubank/state-flow
- 202A debugger for Clojure and ClojureScript with some unique features.
https://github.com/flow-storm/flow-storm-debugger
A debugger for Clojure and ClojureScript with some unique features. - flow-storm/flow-storm-debugger
- 203Deprecated in favor of https://github.com/facebook/duckling
https://github.com/wit-ai/duckling
Deprecated in favor of https://github.com/facebook/duckling - facebookarchive/duckling_old
- 204Realtime web comms library for Clojure/Script
https://github.com/ptaoussanis/sente
Realtime web comms library for Clojure/Script. Contribute to taoensso/sente development by creating an account on GitHub.
- 205Hiccup-style generation of Graphviz graphs in Clojure
https://github.com/daveray/dorothy
Hiccup-style generation of Graphviz graphs in Clojure - daveray/dorothy
- 206A Clojure machine learning library
https://github.com/scicloj/scicloj.ml
A Clojure machine learning library. Contribute to scicloj/scicloj.ml development by creating an account on GitHub.
- 207Clojure Interactive Development Environment for Vim8/Neovim
https://github.com/liquidz/vim-iced
Clojure Interactive Development Environment for Vim8/Neovim - liquidz/vim-iced
- 208Distributed, masterless, high performance, fault tolerant data processing
https://github.com/onyx-platform/onyx
Distributed, masterless, high performance, fault tolerant data processing - onyx-platform/onyx
- 209There's a function for that!
https://github.com/jonase/kibit
There's a function for that! Contribute to clj-commons/kibit development by creating an account on GitHub.
- 210A rules engine for Clojure(Script)
https://github.com/oakes/odoyle-rules
A rules engine for Clojure(Script). Contribute to oakes/odoyle-rules development by creating an account on GitHub.
- 211Graph library for Clojure. Mailing list https://groups.google.com/forum/#!forum/loom-clj
https://github.com/aysylu/loom
Graph library for Clojure. Mailing list https://groups.google.com/forum/#!forum/loom-clj - aysylu/loom
- 212Bidirectional, data-driven RSS/Atom feed consumer, producer and feeds aggregator
https://github.com/alekseysotnikov/buran
Bidirectional, data-driven RSS/Atom feed consumer, producer and feeds aggregator - alekseysotnikov/buran
- 213Compact pretty printer
https://github.com/cgrand/packed-printer
Compact pretty printer. Contribute to cgrand/packed-printer development by creating an account on GitHub.
- 214The missing tool
https://github.com/razum2um/clj-debugger
The missing tool. Contribute to razum2um/clj-debugger development by creating an account on GitHub.
- 215Emacs rainbow delimiters mode
https://github.com/Fanael/rainbow-delimiters
Emacs rainbow delimiters mode. Contribute to Fanael/rainbow-delimiters development by creating an account on GitHub.
- 216Slamhound rips your namespace form apart and reconstructs it.
https://github.com/technomancy/slamhound
Slamhound rips your namespace form apart and reconstructs it. - technomancy/slamhound
- 217Sunsetting Atom
https://atom.io/packages/proto-repl
We are archiving Atom and all projects under the Atom organization for an official sunset on December 15, 2022.
- 218managing Clojure and ClojureScript app state since (reset)
https://github.com/tolitius/mount
managing Clojure and ClojureScript app state since (reset) - tolitius/mount
- 219Static analyzer and linter for Clojure code that sparks joy
https://github.com/borkdude/clj-kondo
Static analyzer and linter for Clojure code that sparks joy - clj-kondo/clj-kondo
- 220Erlang-style supervisor error handling for Clojure
https://github.com/MichaelDrogalis/dire
Erlang-style supervisor error handling for Clojure - MichaelDrogalis/dire
- 221Clojure & ClojureScript Interactive Programming for VS Code
https://github.com/BetterThanTomorrow/calva
Clojure & ClojureScript Interactive Programming for VS Code - BetterThanTomorrow/calva
- 222Better exception reporting middleware for Ring.
https://github.com/magnars/prone
Better exception reporting middleware for Ring. Contribute to magnars/prone development by creating an account on GitHub.
- 223Beagle helps you identify keywords, phrases, regexes, and complex search queries of interest in streams of text documents.
https://github.com/tokenmill/beagle
Beagle helps you identify keywords, phrases, regexes, and complex search queries of interest in streams of text documents. - tokenmill/beagle
- 224Build software better, together
https://github.com/uncomplicate/bayadera:
GitHub is where people build software. More than 100 million people use GitHub to discover, fork, and contribute to over 420 million projects.
- 225A Clojure and ClojureScript game library
https://github.com/oakes/play-cljc
A Clojure and ClojureScript game library. Contribute to oakes/play-cljc development by creating an account on GitHub.
- 226A Clojure library for Apache Spark: fast, fully-features, and developer friendly
https://github.com/gorillalabs/sparkling
A Clojure library for Apache Spark: fast, fully-features, and developer friendly - GitHub - gorillalabs/sparkling: A Clojure library for Apache Spark: fast, fully-features, and developer friendly
- 227A library designed to bridge the gap between the triad of CLJ/CLJS, web-sockets and core.async.
https://github.com/jarohen/chord
A library designed to bridge the gap between the triad of CLJ/CLJS, web-sockets and core.async. - jarohen/chord
- 228Streaming Histograms for Clojure/Java
https://github.com/bigmlcom/histogram
Streaming Histograms for Clojure/Java. Contribute to bigmlcom/histogram development by creating an account on GitHub.
- 229some ideas which are almost good
https://github.com/ztellman/potemkin
some ideas which are almost good. Contribute to clj-commons/potemkin development by creating an account on GitHub.
- 230The Automagic Project Planner
https://github.com/turbopape/milestones
The Automagic Project Planner. Contribute to turbopape/milestones development by creating an account on GitHub.
- 231Bash-like shell based on Clojure
https://github.com/dundalek/closh
Bash-like shell based on Clojure. Contribute to dundalek/closh development by creating an account on GitHub.
- 232Configuration powertool with `metosin/malli`
https://github.com/piotr-yuxuan/malli-cli
Configuration powertool with `metosin/malli`. Contribute to piotr-yuxuan/malli-cli development by creating an account on GitHub.
- 233Fast, Clojure-based rule engine
https://github.com/yipeeio/arete
Fast, Clojure-based rule engine. Contribute to yipeeio/arete development by creating an account on GitHub.
- 234Practice Clojure using Interactive Programming in your editor
https://github.com/PEZ/rich4clojure
Practice Clojure using Interactive Programming in your editor - PEZ/rich4clojure
- 235Full featured next gen Clojure test runner
https://github.com/lambdaisland/kaocha
Full featured next gen Clojure test runner. Contribute to lambdaisland/kaocha development by creating an account on GitHub.
- 236Redis client + message queue for Clojure
https://github.com/ptaoussanis/carmine
Redis client + message queue for Clojure. Contribute to taoensso/carmine development by creating an account on GitHub.
- 237A validation DSL for Clojure & Clojurescript applications
https://github.com/leonardoborges/bouncer
A validation DSL for Clojure & Clojurescript applications - theleoborges/bouncer
- 238Ultra-lightweight literate programming for clojure inspired by docco
https://github.com/gdeer81/marginalia
Ultra-lightweight literate programming for clojure inspired by docco - clj-commons/marginalia
- 239Natural Language Processing in Clojure (opennlp)
https://github.com/dakrone/clojure-opennlp
Natural Language Processing in Clojure (opennlp). Contribute to dakrone/clojure-opennlp development by creating an account on GitHub.
- 240Library for helping print things prettily, in Clojure - ANSI fonts, formatted exceptions
https://github.com/AvisoNovate/pretty
Library for helping print things prettily, in Clojure - ANSI fonts, formatted exceptions - clj-commons/pretty
- 241A fast, Django inspired template system in Clojure.
https://github.com/yogthos/Selmer
A fast, Django inspired template system in Clojure. - yogthos/Selmer
- 242Engelberg/instaparse
https://github.com/Engelberg/instaparse
Contribute to Engelberg/instaparse development by creating an account on GitHub.
- 243Grep-like utility based on Lucene Monitor compiled with GraalVM native-image
https://github.com/dainiusjocas/lucene-grep
Grep-like utility based on Lucene Monitor compiled with GraalVM native-image - dainiusjocas/lucene-grep
- 244Server-side application framework for Clojure
https://github.com/weavejester/duct
Server-side application framework for Clojure. Contribute to duct-framework/duct development by creating an account on GitHub.
- 245Recompile Java code without restarting the REPL
https://github.com/ztellman/virgil
Recompile Java code without restarting the REPL. Contribute to clj-commons/virgil development by creating an account on GitHub.
- 246Figwheel builds your ClojureScript code and hot loads it into the browser as you are coding!
https://github.com/bhauman/lein-figwheel
Figwheel builds your ClojureScript code and hot loads it into the browser as you are coding! - bhauman/lein-figwheel
- 247Seesaw turns the Horror of Swing into a friendly, well-documented, Clojure library
https://github.com/daveray/seesaw
Seesaw turns the Horror of Swing into a friendly, well-documented, Clojure library - clj-commons/seesaw
- 248Forward-chaining rules in Clojure(Script)
https://github.com/cerner/clara-rules
Forward-chaining rules in Clojure(Script). Contribute to oracle-samples/clara-rules development by creating an account on GitHub.
- 249Asynchronous streaming communication for Clojure - web server, web client, and raw TCP/UDP
https://github.com/ztellman/aleph
Asynchronous streaming communication for Clojure - web server, web client, and raw TCP/UDP - clj-commons/aleph
- 250Pattern matching for the monads in the cats Clojure library
https://github.com/zalando/cats.match
Pattern matching for the monads in the cats Clojure library - zalando-stups/cats.match
- 251yetanalytics/dl4clj
https://github.com/yetanalytics/dl4clj
Contribute to yetanalytics/dl4clj development by creating an account on GitHub.
- 252Pure Clojure Webdriver protocol implementation
https://github.com/igrishaev/etaoin
Pure Clojure Webdriver protocol implementation. Contribute to clj-commons/etaoin development by creating an account on GitHub.
- 253Build software better, together
https://github.com/uncomplicate/neanderthal:
GitHub is where people build software. More than 100 million people use GitHub to discover, fork, and contribute to over 420 million projects.
- 254Build software better, together
https://github.com/linpengcheng/ClojureBoxNpp:
GitHub is where people build software. More than 100 million people use GitHub to discover, fork, and contribute to over 420 million projects.
- 255Turn Clojure data structures into SQL
https://github.com/jkk/honeysql
Turn Clojure data structures into SQL. Contribute to seancorfield/honeysql development by creating an account on GitHub.
- 256Clojure lint tool
https://github.com/jonase/eastwood
Clojure lint tool. Contribute to jonase/eastwood development by creating an account on GitHub.
- 257High-performance data-driven data specification library for Clojure/Script.
https://github.com/metosin/malli
High-performance data-driven data specification library for Clojure/Script. - metosin/malli
- 258A community coding style guide for the Clojure programming language
https://github.com/bbatsov/clojure-style-guide
A community coding style guide for the Clojure programming language - bbatsov/clojure-style-guide
Related Articlesto learn about angular.
- 1Clojure for Beginners: A Functional Approach to Programming
- 2Understanding Clojure's Lisp Syntax
- 3Building a Simple Web App: Web Development in Clojure
- 4Full-Stack Development with Clojure and ClojureScript
- 5Data Manipulation in Clojure using Sequences and Collections
- 6Integrating Clojure with Databases: A Guide to Datomic and JDBC
- 7Concurrency in Clojure: Working with Atoms, Refs, and Agents
- 8Parallelism in Clojure: core.async and Futures
- 9Clojure Macros: Writing Code That Writes Code
- 10Building a Real-Time Application with Clojure: A WebSocket-Based Chat App
FAQ'sto learn more about Angular JS.
mail [email protected] to add more queries here 🔍.
- 1
why clojure is used
- 2
why learn clojure
- 3
where is clojure used
- 4
is clojure a functional programming language
- 5
who uses clojure programming language
- 6
is clojurescript dead
- 7
what is clojure used for
- 8
who uses clojure
- 9
who programmed programming
- 10
is clojure fast
- 11
is clojure dying
- 12
who developed cobol programming language
- 13
how to learn clojure
- 14
what is clojure
- 15
what is clojure programming language
- 16
is clojure hard to learn
- 17
how popular is clojure
- 18
how to run clojure
- 19
when to use clojure
- 20
is clojure slow
- 21
which companies use clojure
- 22
do clojure used today
- 23
why use clojure
- 24
why is clojure not popular
- 25
is clojure a lisp
- 26
what is clojure language
- 27
when clojure made
- 28
why is clojure so highly paid
- 29
who created cobol programming language
- 30
is clojure worth learning
- 31
why to learn and use clojure
- 32
is clojure dead
More Sitesto check out once you're finished browsing here.
https://www.0x3d.site/
0x3d is designed for aggregating information.
https://nodejs.0x3d.site/
NodeJS Online Directory
https://cross-platform.0x3d.site/
Cross Platform Online Directory
https://open-source.0x3d.site/
Open Source Online Directory
https://analytics.0x3d.site/
Analytics Online Directory
https://javascript.0x3d.site/
JavaScript Online Directory
https://golang.0x3d.site/
GoLang Online Directory
https://python.0x3d.site/
Python Online Directory
https://swift.0x3d.site/
Swift Online Directory
https://rust.0x3d.site/
Rust Online Directory
https://scala.0x3d.site/
Scala Online Directory
https://ruby.0x3d.site/
Ruby Online Directory
https://clojure.0x3d.site/
Clojure Online Directory
https://elixir.0x3d.site/
Elixir Online Directory
https://elm.0x3d.site/
Elm Online Directory
https://lua.0x3d.site/
Lua Online Directory
https://c-programming.0x3d.site/
C Programming Online Directory
https://cpp-programming.0x3d.site/
C++ Programming Online Directory
https://r-programming.0x3d.site/
R Programming Online Directory
https://perl.0x3d.site/
Perl Online Directory
https://java.0x3d.site/
Java Online Directory
https://kotlin.0x3d.site/
Kotlin Online Directory
https://php.0x3d.site/
PHP Online Directory
https://react.0x3d.site/
React JS Online Directory
https://angular.0x3d.site/
Angular JS Online Directory