{"id":13491281,"url":"https://github.com/metosin/malli","last_synced_at":"2025-05-11T14:00:15.498Z","repository":{"id":38368636,"uuid":"187269923","full_name":"metosin/malli","owner":"metosin","description":"High-performance data-driven data specification library for Clojure/Script.","archived":false,"fork":false,"pushed_at":"2025-05-08T07:38:55.000Z","size":8009,"stargazers_count":1585,"open_issues_count":183,"forks_count":219,"subscribers_count":36,"default_branch":"master","last_synced_at":"2025-05-11T14:00:06.924Z","etag":null,"topics":["clojure","clojurescript","coercion","error-messages","generators","inferring-schemas","json-schema","metosin-active","modelling","transformation","validation","visualizing-schemas"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"epl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/metosin.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2019-05-17T19:21:51.000Z","updated_at":"2025-05-09T05:21:48.000Z","dependencies_parsed_at":"2023-12-24T15:27:45.046Z","dependency_job_id":"9bcb8690-a13b-41e5-b34f-c75f2eebd72e","html_url":"https://github.com/metosin/malli","commit_stats":{"total_commits":2182,"total_committers":133,"mean_commits":"16.406015037593985","dds":"0.34647112740604946","last_synced_commit":"bc80f3f0a3df5ee68634e5ce2f63182d98fa106e"},"previous_names":[],"tags_count":48,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/metosin%2Fmalli","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/metosin%2Fmalli/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/metosin%2Fmalli/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/metosin%2Fmalli/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/metosin","download_url":"https://codeload.github.com/metosin/malli/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253576264,"owners_count":21930169,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["clojure","clojurescript","coercion","error-messages","generators","inferring-schemas","json-schema","metosin-active","modelling","transformation","validation","visualizing-schemas"],"created_at":"2024-07-31T19:00:55.193Z","updated_at":"2025-05-11T14:00:15.401Z","avatar_url":"https://github.com/metosin.png","language":"Clojure","funding_links":[],"categories":["Clojure","clojure","Data Validation"],"sub_categories":[],"readme":"# malli\n\n[![Build Status](https://github.com/metosin/malli/actions/workflows/clojure.yml/badge.svg)](https://github.com/metosin/malli/actions)\n[![cljdoc badge](https://cljdoc.org/badge/metosin/malli)](https://cljdoc.org/d/metosin/malli/)\n[![Clojars Project](https://img.shields.io/clojars/v/metosin/malli.svg)](https://clojars.org/metosin/malli)\n[![Slack](https://img.shields.io/badge/clojurians-malli-blue.svg?logo=slack)](https://clojurians.slack.com/messages/malli/)\n[![bb compatible](https://raw.githubusercontent.com/babashka/babashka/master/logo/badge.svg)](https://book.babashka.org#badges)\n\nData-driven Schemas for Clojure/Script and [babashka](#babashka).\n\n[Metosin Open Source Status: Active](https://github.com/metosin/open-source/blob/main/project-status.md#active). Stability: well matured [*alpha*](#alpha).\n\n\u003cimg src=\"docs/img/malli.png\" width=130 align=\"right\"/\u003e\n\n- Schema definitions as data\n- [Vector](#vector-syntax), [Map](#map-syntax) and [Lite](#lite) syntaxes\n- [Validation](#validation) and [Value Transformation](#value-transformation)\n- First class [Error Messages](#error-messages) with [Spell Checking](#spell-checking)\n- [Generating values](#value-generation) from Schemas\n- [Inferring Schemas](#inferring-schemas) from sample values and [Destructuring](#destructuring).\n- Tools for [Programming with Schemas](#programming-with-schemas)\n- [Parsing](#parsing-values) and [Unparsing](#unparsing-values) values\n- [Enumeration](#enumeration-schemas), [Sequence](#sequence-schemas), [Vector](#vector-schemas), and [Set](#set-schemas) Schemas\n- [Persisting schemas](#persisting-schemas), even [function schemas](#serializable-functions)\n- Immutable, Mutable, Dynamic, Lazy and Local [Schema Registries](#schema-registry)\n- [Schema Transformations](#schema-Transformation) to [JSON Schema](#json-schema), [Swagger2](#swagger2), and [descriptions in english](#description)\n- [Multi-schemas](#multi-schemas), [Recursive Schemas](#recursive-schemas) and [Default values](#default-values)\n- [Function Schemas](docs/function-schemas.md) with dynamic and static schema checking\n   - Integrates with both [clj-kondo](#clj-kondo) and [Typed Clojure](#static-type-checking-via-typed-clojure)\n- Visualizing Schemas with [DOT](#dot) and [PlantUML](#plantuml)\n- Pretty [development time errors](#pretty-errors)\n- [Fast](#performance)\n\nPresentations:\n\n- [Transforming Data With Malli and Meander](https://www.metosin.fi/blog/transforming-data-with-malli-and-meander/)\n- [High-Performance Schemas in Clojure/Script with Malli 1/2](https://www.metosin.fi/blog/high-performance-schemas-in-clojurescript-with-malli-1-2/)\n- [ClojureStream Podcast: Malli wtih Tommi Reiman](https://soundcloud.com/clojurestream/s4-e30-malli-wtih-tommi-reiman)\n- [Structure and Interpretation of Malli Regex Schemas](https://www.metosin.fi/blog/malli-regex-schemas/)\n- LNDCLJ 9.12.2020: [Designing with Malli](https://youtu.be/bQDkuF6-py4), slides [here](https://www.slideshare.net/mobile/metosin/designing-with-malli)\n- [Malli, Data-Driven Schemas for Clojure/Script](https://www.metosin.fi/blog/malli/)\n- CEST 2.6.2020: [Data-driven Rapid Application Development with Malli](https://www.youtube.com/watch?v=ww9yR_rbgQs)\n- ClojureD 2020: [Malli: Inside Data-driven Schemas](https://www.youtube.com/watch?v=MR83MhWQ61E), slides [here](https://www.slideshare.net/metosin/malli-inside-datadriven-schemas)\n\nTry the [online demo](https://malli.io), see also some [3rd Party Libraries](#3rd-party-libraries).\n\nWant to contribute? See the [Development](#development) guide.\n\n\u003cimg src=\"docs/img/malli-defn.png\" width=\"600\" /\u003e\n\n\u003e Hi! We are [Metosin](https://metosin.fi), a consulting company. These libraries have evolved out of the work we do for our clients.\n\u003e We maintain \u0026 develop this project, for you, for free. Issues and pull requests welcome!\n\u003e However, if you want more help using the libraries, or want us to build something as cool for you, consider our [commercial support](https://www.metosin.fi/en/open-source-support).\n\n## Motivation\n\nWe 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](https://json-schema.org/), but for Clojure/Script.\n\nHasn't the problem been solved (many times) already?\n\nThere is [Schema](https://github.com/plumatic/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 \u0026 de-serializing schemas is non-trivial and there is no proper support on branching.\n\n[Spec](https://clojure.org/guides/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](https://github.com/metosin/spec-tools) was created to \"fix\" some of the things, but after [five years](https://github.com/metosin/spec-tools/commit/18aeb78db7886c985b2881fd87fde6039128b3fb) of developing it, it's still a kind of hack and not fun to maintain.\n\nSo, 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.\n\n\u003e 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.\n\n- Rich Hickey, [Open Source is Not About You](https://gist.github.com/richhickey/1563cddea1002958f96e7ba9519972d9)\n\n## The library\n\n[![Clojars Project](http://clojars.org/metosin/malli/latest-version.svg)](http://clojars.org/metosin/malli)\n\nMalli requires Clojure 1.11 or ClojureScript 1.11.51.\n\nMalli is tested with the LTS releases Java 8, 11, 17 and 21.\n\n## Quickstart\n\n```clojure\n(require '[malli.core :as m])\n\n(def UserId :string)\n\n(def Address\n  [:map\n   [:street :string]\n   [:country [:enum \"FI\" \"UA\"]]])\n\n(def User\n  [:map\n   [:id #'UserId]\n   [:address #'Address]\n   [:friends [:set {:gen/max 2} [:ref #'User]]]])\n\n(require '[malli.generator :as mg])\n\n(mg/generate User)\n;{:id \"AC\",\n; :address {:street \"mf\", :country \"UA\"},\n; :friends #{{:id \"1dm\",\n;             :address {:street \"8\", :country \"UA\"},\n;             :friends #{}}}}\n\n(m/validate User *1)\n; =\u003e true\n```\n\n## Syntax\n\nMalli supports [Vector](#vector-syntax), [Map](#map-syntax) and [Lite](#lite) syntaxes.\n\n### Vector syntax\n\nThe default syntax uses vectors, inspired by [hiccup](https://github.com/weavejester/hiccup):\n\n```clojure\ntype\n[type \u0026 children]\n[type properties \u0026 children]\n```\nExamples:\n\n```clojure\n;; just a type (String)\n:string\n\n;; type with properties\n[:string {:min 1, :max 10}]\n\n;; type with properties and children\n[:tuple {:title \"location\"} :double :double]\n\n;; a function schema of :int -\u003e :int\n[:=\u003e [:cat :int] :int]\n[:-\u003e :int :int]\n```\n\nUsage:\n\n```clojure\n(require '[malli.core :as m])\n\n(def non-empty-string\n  (m/schema [:string {:min 1}]))\n\n(m/schema? non-empty-string)\n; =\u003e true\n\n(m/validate non-empty-string \"\")\n; =\u003e false\n\n(m/validate non-empty-string \"kikka\")\n; =\u003e true\n\n(m/form non-empty-string)\n; =\u003e [:string {:min 1}]\n```\n\n### Map syntax\n\nAlternative map-syntax, similar to [cljfx](https://github.com/cljfx/cljfx):\n\n**NOTE**: For now, Map syntax in considered as internal, so don't use it as a database persistency model.\n\n```clojure\n;; just a type (String)\n{:type :string}\n\n;; type with properties\n{:type :string\n :properties {:min 1, :max 10}\n\n;; type with properties and children\n{:type :tuple\n :properties {:title \"location\"}\n :children [{:type :double}\n            {:type :double}]}\n\n;; a function schema of :int -\u003e :int\n{:type :=\u003e\n :input {:type :cat, :children [{:type :int}]}\n :output :int}\n{:type :-\u003e\n :children [{:type :int} {:type :int}]}\n```\n\nUsage:\n\n```clojure\n(def non-empty-string\n  (m/from-ast {:type :string\n               :properties {:min 1}}))\n\n(m/schema? non-empty-string)\n; =\u003e true\n\n(m/validate non-empty-string \"\")\n; =\u003e false\n\n(m/validate non-empty-string \"kikka\")\n; =\u003e true\n\n(m/ast non-empty-string)\n; =\u003e {:type :string,\n;     :properties {:min 1}}\n```\n\nMap-syntax is also called the [Schema AST](#schema-ast).\n\n### Why multiple syntaxes?\n\nMalli started with just the [Vector syntax](#vector-syntax). It's really powerful and relatively easy to read, but not optimal for all use cases.\n\nWe introduced [Map Syntax](#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.\n\nWe added [Lite Syntax](#lite) for simplified schema creation for special cases, like to be used with [reitit coercion](https://cljdoc.org/d/metosin/reitit/CURRENT/doc/coercion/malli) and for easy migration from [data-specs](https://cljdoc.org/d/metosin/spec-tools/CURRENT/doc/data-specs).\n\n## Example Address schema\n\nFollowing example schema is assumed in many of the following examples.\n\n```clojure\n(def Address\n  [:map\n   [:id string?]\n   [:tags [:set keyword?]]\n   [:address\n    [:map\n     [:street string?]\n     [:city string?]\n     [:zip int?]\n     [:lonlat [:tuple double? double?]]]]])\n```\n\n## Validation\n\nValidating values against a schema:\n\n```clojure\n;; with schema instances\n(m/validate (m/schema :int) 1)\n; =\u003e true\n\n;; with vector syntax\n(m/validate :int 1)\n; =\u003e true\n\n(m/validate :int \"1\")\n; =\u003e false\n\n(m/validate [:= 1] 1)\n; =\u003e true\n\n(m/validate [:enum 1 2] 1)\n; =\u003e true\n\n(m/validate [:and :int [:\u003e 6]] 7)\n; =\u003e true\n\n(m/validate [:qualified-keyword {:namespace :aaa}] :aaa/bbb)\n; =\u003e true\n\n;; optimized (pure) validation function for best performance\n(def valid?\n  (m/validator\n    [:map\n     [:x :boolean]\n     [:y {:optional true} :int]\n     [:z :string]]))\n\n(valid? {:x true, :z \"kikka\"})\n; =\u003e true\n```\n\nSchemas can have properties:\n\n```clojure\n(def Age\n  [:and\n   {:title \"Age\"\n    :description \"It's an age\"\n    :json-schema/example 20}\n   :int [:\u003e 18]])\n\n(m/properties Age)\n; =\u003e {:title \"Age\"\n;     :description \"It's an age\"\n;     :json-schema/example 20}\n```\n\nMaps are open by default:\n\n```clojure\n(m/validate\n  [:map [:x :int]]\n  {:x 1, :extra \"key\"})\n; =\u003e true\n```\n\nMaps can be closed with `:closed` property:\n\n```clojure\n(m/validate\n  [:map {:closed true} [:x :int]]\n  {:x 1, :extra \"key\"})\n; =\u003e false\n```\n\nMaps keys are not limited to keywords:\n\n```clojure\n(m/validate\n  [:map\n   [\"status\" [:enum \"ok\"]]\n   [1 :any]\n   [nil :any]\n   [::a :string]]\n  {\"status\" \"ok\"\n   1 'number\n   nil :yay\n   ::a \"properly awesome\"})\n; =\u003e true\n```\n\nMost core-predicates are mapped to Schemas:\n\n```clojure\n(m/validate string? \"kikka\")\n; =\u003e true\n```\n\n*NOTE*: Predicate Schemas do not cover any schema properties, e.g. `string?` can't be modified with properties like `:min` and `:max`. If you want to use the schema properties, use real schema types instead, e.g. `:string` over `string?`.\n\nSee [the full list of default schemas](#schema-registry).\n\n## Enumeration schemas\n\n`:enum` schemas `[:enum V1 V2 ...]` represent an enumerated set of values `V1 V2 ...`.\n\nThis 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,\nshrinking to the left-most value.\n\nThere are some special cases to keep in mind around syntax. Since schema properties can be specified with a map or nil, enumerations starting with\na map or nil must use slightly different syntax.\n\nIf your `:enum` does not have properties, you must provide `nil` as the properties.\n\n```clojure\n[:enum nil {}]  ;; singleton schema of {}\n[:enum nil nil] ;; singleton schema of nil\n```\n\nIf your `:enum` has properties, the leading map with be interpreted as properties, not an enumerated value.\n\n```clojure\n[:enum {:foo :bar} {}]  ;; singleton schema of {}, with properties {:foo :bar}\n[:enum {:foo :bar} nil] ;; singleton schema of nil, with properties {:foo :bar}\n```\n\nIn 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.\n\n## Qualified keys in a map\n\nYou can also use [decomplected maps keys and values](https://clojure.org/about/spec#_decomplect_mapskeysvalues) using registry references. References must be either qualified keywords or strings.\n\n```clojure\n(m/validate\n  [:map {:registry {::id int?\n                    ::country string?}}\n   ::id\n   [:name string?]\n   [::country {:optional true}]]\n  {::id 1\n   :name \"kikka\"})\n; =\u003e true\n```\n\n## Homogeneous maps\n\nOther times, we use a map as a homogeneous index. In this case, all our key-value\npairs have the same type. For this use case, we can use the `:map-of` schema.\n\n```clojure\n(m/validate\n  [:map-of :string [:map [:lat number?] [:long number?]]]\n  {\"oslo\" {:lat 60 :long 11}\n   \"helsinki\" {:lat 60 :long 24}})\n;; =\u003e true\n```\n## Map with default schemas\n\nMap schemas can define a special `:malli.core/default` key to handle extra keys:\n\n```clojure\n(m/validate\n [:map\n  [:x :int]\n  [:y :int]\n  [::m/default [:map-of :int :int]]]\n {:x 1, :y 2, 1 1, 2 2})\n; =\u003e true\n```\ndefault branching can be arbitrarily nested:\n\n```clojure\n(m/validate\n [:map\n  [:x :int]\n  [::m/default [:map\n                [:y :int]\n                [::m/default [:map-of :int :int]]]]]\n {:x 1, :y 2, 1 1, 2 2})\n; =\u003e true\n```\n\n## Seqable schemas\n\nThe `:seqable` and `:every` schemas describe `seqable?` collections. They\ndiffer in their handling of collections that are neither `counted?` nor `indexed?`, and their\n[parsers](#parsing-values):\n1. `:seqable` parses its elements but `:every` does not and returns the identical input, and\n2. valid unparsed `:seqable` values lose the original collection type while `:every`\n   returns the identical input.\n\n`:seqable` validates the entire collection, while `:every` checks only the\nlargest of `:min`, `(inc :max)`, and `(::m/coll-check-limit options 101)`, or\nthe entire collection if the input is `counted?` or `indexed?`.\n\n```clojure\n;; :seqable and :every validate identically with small, counted, or indexed collections.\n(m/validate [:seqable :int] #{1 2 3})\n;=\u003e true\n(m/validate [:seqable :int] [1 2 3])\n;=\u003e true\n(m/validate [:seqable :int] (sorted-set 1 2 3))\n;=\u003e true\n(m/validate [:seqable :int] (range 1000))\n;=\u003e true\n(m/validate [:seqable :int] (conj (vec (range 1000)) nil))\n;=\u003e false\n\n(m/validate [:every :int] #{1 2 3})\n;=\u003e true\n(m/validate [:every :int] [1 2 3])\n;=\u003e true\n(m/validate [:every :int] (sorted-set 1 2 3))\n;=\u003e true\n(m/validate [:every :int] (vec (range 1000)))\n;=\u003e true\n(m/validate [:every :int] (conj (vec (range 1000)) nil))\n;=\u003e false\n\n;; for large uncounted and unindexed collections, :every only checks a certain length\n(m/validate [:seqable :int] (concat (range 1000) [nil]))\n;=\u003e false\n(m/validate [:every :int] (concat (range 1000) [nil]))\n;=\u003e true\n```\n\n\n## Sequence schemas\n\nYou can use `:sequential` to describe homogeneous sequential Clojure collections.\n\n```clojure\n(m/validate [:sequential any?] (list \"this\" 'is :number 42))\n;; =\u003e true\n\n(m/validate [:sequential int?] [42 105])\n;; =\u003e true\n\n(m/validate [:sequential int?] #{42 105})\n;; =\u003e false\n```\n\nMalli also supports sequence regexes (also called sequence expressions) like [Seqexp](https://github.com/cgrand/seqexp) and Spec.\nThe supported operators are `:cat` \u0026 `:catn` for concatenation / sequencing\n\n```clojure\n(m/validate [:cat string? int?] [\"foo\" 0]) ; =\u003e true\n\n(m/validate [:catn [:s string?] [:n int?]] [\"foo\" 0]) ; =\u003e true\n```\n\n`:alt` \u0026 `:altn` for alternatives\n\n```clojure\n(m/validate [:alt keyword? string?] [\"foo\"]) ; =\u003e true\n\n(m/validate [:altn [:kw keyword?] [:s string?]] [\"foo\"]) ; =\u003e true\n```\n\nand `:?`, `:*`, `:+` \u0026 `:repeat` for repetition:\n\n```clojure\n(m/validate [:? int?] []) ; =\u003e true\n(m/validate [:? int?] [1]) ; =\u003e true\n(m/validate [:? int?] [1 2]) ; =\u003e false\n\n(m/validate [:* int?] []) ; =\u003e true\n(m/validate [:* int?] [1 2 3]) ; =\u003e true\n\n(m/validate [:+ int?] []) ; =\u003e false\n(m/validate [:+ int?] [1]) ; =\u003e true\n(m/validate [:+ int?] [1 2 3]) ; =\u003e true\n\n(m/validate [:repeat {:min 2, :max 4} int?] [1]) ; =\u003e false\n(m/validate [:repeat {:min 2, :max 4} int?] [1 2]) ; =\u003e true\n(m/validate [:repeat {:min 2, :max 4} int?] [1 2 3 4]) ; =\u003e true (:max is inclusive, as elsewhere in Malli)\n(m/validate [:repeat {:min 2, :max 4} int?] [1 2 3 4 5]) ; =\u003e false\n```\n\n`:catn` and `:altn` allow naming the subsequences / alternatives\n\n```clojure\n(m/explain\n  [:* [:catn [:prop string?] [:val [:altn [:s string?] [:b boolean?]]]]]\n  [\"-server\" \"foo\" \"-verbose\" 11 \"-user\" \"joe\"])\n;; =\u003e {:schema [:* [:catn [:prop string?] [:val [:altn [:s string?] [:b boolean?]]]]],\n;;     :value [\"-server\" \"foo\" \"-verbose\" 11 \"-user\" \"joe\"],\n;;     :errors ({:path [0 :val :s], :in [3], :schema string?, :value 11}\n;;              {:path [0 :val :b], :in [3], :schema boolean?, :value 11})}\n```\n\nwhile `:cat` and `:alt` just use numeric indices for paths:\n\n```clojure\n(m/explain\n  [:* [:cat string? [:alt string? boolean?]]]\n  [\"-server\" \"foo\" \"-verbose\" 11 \"-user\" \"joe\"])\n;; =\u003e {:schema [:* [:cat string? [:alt string? boolean?]]],\n;;     :value [\"-server\" \"foo\" \"-verbose\" 11 \"-user\" \"joe\"],\n;;     :errors ({:path [0 1 0], :in [3], :schema string?, :value 11}\n;;              {:path [0 1 1], :in [3], :schema boolean?, :value 11})}\n```\n\nAs all these examples show, the sequence expression (seqex) operators take any non-seqex child schema to\nmean a sequence of one element that matches that schema. To force that behaviour for\na seqex child `:schema` can be used:\n\n```clojure\n(m/validate\n  [:cat [:= :names] [:schema [:* string?]] [:= :nums] [:schema [:* number?]]]\n  [:names [\"a\" \"b\"] :nums [1 2 3]])\n; =\u003e true\n\n;; whereas\n(m/validate\n  [:cat [:= :names] [:* string?] [:= :nums] [:* number?]]\n  [:names \"a\" \"b\" :nums 1 2 3])\n; =\u003e true\n```\n\nAlthough a lot of effort has gone into making the seqex implementation fast\n\n```clojure\n(require '[clojure.spec.alpha :as s])\n(require '[criterium.core :as cc])\n\n(let [valid? (partial s/valid? (s/* int?))]\n  (cc/quick-bench (valid? (range 10)))) ; Execution time mean : 27µs\n\n(let [valid? (m/validator [:* int?])]\n  (cc/quick-bench (valid? (range 10)))) ; Execution time mean : 2.7µs\n```\n\nit is always better to use less general tools whenever possible:\n\n```clojure\n(let [valid? (partial s/valid? (s/coll-of int?))]\n  (cc/quick-bench (valid? (range 10)))) ; Execution time mean : 1.8µs\n\n(let [valid? (m/validator [:sequential int?])]\n  (cc/quick-bench (valid? (range 10)))) ; Execution time mean : 0.12µs\n```\n\n## Vector schemas\n\nYou can use `:vector` to describe homogeneous Clojure vectors.\n\n```clojure\n(m/validate [:vector int?] [1 2 3])\n;; =\u003e true\n\n(m/validate [:vector int?] (list 1 2 3))\n;; =\u003e false\n```\n\nA `:tuple` schema describes a fixed length Clojure vector of heterogeneous elements:\n\n```clojure\n(m/validate [:tuple keyword? string? number?] [:bing \"bang\" 42])\n;; =\u003e true\n```\n\nTo create a vector schema based on a seqex, use `:and`.\n\n```clojure\n;; non-empty vector starting with a keyword\n(m/validate [:and [:cat :keyword [:* :any]]\n                  vector?]\n            [:a 1])\n; =\u003e true\n\n(m/validate [:and [:cat :keyword [:* :any]]\n                  vector?]\n            (:a 1))\n; =\u003e false\n```\n\nNote: To generate values from a vector seqex, see [:and generation](#and-generation).\n\n## Set schemas\n\nYou can use `:set` to describe homogeneous Clojure sets.\n\n```clojure\n(m/validate [:set int?] #{42 105})\n;; =\u003e true\n\n(m/validate [:set int?] #{:a :b})\n;; =\u003e false\n```\n\n## String schemas\n\nUsing a predicate:\n\n```clojure\n(m/validate string? \"kikka\")\n```\n\nUsing `:string` Schema:\n\n```clojure\n(m/validate :string \"kikka\")\n;; =\u003e true\n\n(m/validate [:string {:min 1, :max 4}] \"\")\n;; =\u003e false\n```\n\nUsing regular expressions:\n\n```clojure\n\n(m/validate #\"a+b+c+\" \"abbccc\")\n;; =\u003e true\n\n;; :re with string\n(m/validate [:re \".{3,5}\"] \"abc\")\n;; =\u003e true\n\n;; :re with regex\n(m/validate [:re #\".{3,5}\"] \"abc\")\n;; =\u003e true\n\n;; NB: re-find semantics\n(m/validate [:re #\"\\d{4}\"] \"1234567\")\n;; =\u003e true\n\n;; anchor with ^...$ if you want to strictly match the whole string\n(m/validate [:re #\"^\\d{4}$\"] \"1234567\")\n;; =\u003e false\n\n```\n\n## Maybe schemas\n\nUse `:maybe` to express that an element should match some schema OR be `nil`:\n\n```clojure\n(m/validate [:maybe string?] \"bingo\")\n;; =\u003e true\n\n(m/validate [:maybe string?] nil)\n;; =\u003e true\n\n(m/validate [:maybe string?] :bingo)\n;; =\u003e false\n```\n\n## Fn schemas\n\n`:fn` allows any predicate function to be used:\n\n```clojure\n(def my-schema\n  [:and\n   [:map\n    [:x int?]\n    [:y int?]]\n   [:fn (fn [{:keys [x y]}] (\u003e x y))]])\n\n(m/validate my-schema {:x 1, :y 0})\n; =\u003e true\n\n(m/validate my-schema {:x 1, :y 2})\n; =\u003e false\n```\n\n## Error messages\n\nDetailed errors with `m/explain`:\n\n```clojure\n(m/explain\n  Address\n  {:id \"Lillan\"\n   :tags #{:artesan :coffee :hotel}\n   :address {:street \"Ahlmanintie 29\"\n             :city \"Tampere\"\n             :zip 33100\n             :lonlat [61.4858322, 23.7854658]}})\n; =\u003e nil\n\n(m/explain\n  Address\n  {:id \"Lillan\"\n   :tags #{:artesan \"coffee\" :garden}\n   :address {:street \"Ahlmanintie 29\"\n             :zip 33100\n             :lonlat [61.4858322, nil]}})\n;{:schema [:map\n;          [:id string?]\n;          [:tags [:set keyword?]]\n;          [:address [:map\n;                     [:street string?]\n;                     [:city string?]\n;                     [:zip int?]\n;                     [:lonlat [:tuple double? double?]]]]],\n; :value {:id \"Lillan\",\n;         :tags #{:artesan :garden \"coffee\"},\n;         :address {:street \"Ahlmanintie 29\"\n;                   :zip 33100\n;                   :lonlat [61.4858322 nil]}},\n; :errors ({:path [:tags 0]\n;           :in [:tags 0]\n;           :schema keyword?\n;           :value \"coffee\"}\n;          {:path [:address :city],\n;           :in [:address :city],\n;           :schema [:map\n;                    [:street string?]\n;                    [:city string?]\n;                    [:zip int?]\n;                    [:lonlat [:tuple double? double?]]],\n;           :type :malli.core/missing-key}\n;          {:path [:address :lonlat 1]\n;           :in [:address :lonlat 1]\n;           :schema double?\n;           :value nil})}\n```\n\nUnder `:errors`, you get a list of errors with the following keys:\n\n* `:path`, error location in Schema\n* `:in`, error location in value\n* `:schema`, schema in error\n* `:value`, value in error\n\n```clojure\n(def Schema [:map [:x [:maybe [:tuple :string]]]])\n\n(def value {:x [1]})\n\n(def error (-\u003e Schema\n               (m/explain value)\n               :errors\n               first))\n\nerror\n;{:path [:x 0 0]\n; :in [:x 0]\n; :schema :string\n; :value 1}\n\n(get-in value (:in error))\n; =\u003e 1\n\n(mu/get-in Schema (:path error))\n; =\u003e :string\n```\n\nNote! If you need error messages that serialize neatly to EDN/JSON, use `malli.util/explain-data` instead.\n\n## Humanized error messages\n\nExplain results can be humanized with `malli.error/humanize`:\n\n```clojure\n(require '[malli.error :as me])\n\n(-\u003e Address\n    (m/explain\n      {:id \"Lillan\"\n       :tags #{:artesan \"coffee\" :garden}\n       :address {:street \"Ahlmanintie 29\"\n                 :zip 33100\n                 :lonlat [61.4858322, nil]}})\n    (me/humanize))\n;{:tags #{[\"should be a keyword\"]}\n; :address {:city [\"missing required key\"]\n;           :lonlat [nil [\"should be a double\"]]}}\n```\n\nOr if you already have a malli validation exception (e.g. in a catch form):\n\n```clojure\n(require '[malli.error :as me])\n\n(try\n  (m/assert Address {:not \"an address\"})\n  (catch Exception e\n    (-\u003e e ex-data :data :explain me/humanize)))\n```\n\n## Custom error messages\n\nError messages can be customized with `:error/message` and `:error/fn` properties.\n\nIf `:error/message` is of a predictable structure, it will automatically support custom `[:not schema]` failures for the following locales:\n- `:en` if message starts with `should` or `should not` then they will be swapped automatically. Otherwise, message is ignored.\n```clojure\n;; e.g.,\n(me/humanize\n  (m/explain\n    [:not\n     [:fn {:error/message {:en \"should be a multiple of 3\"}}\n      #(= 0 (mod % 3))]]\n    3))\n; =\u003e [\"should not be a multiple of 3\"]\n```\n\nThe first argument to `:error/fn` is a map with keys:\n- `:schema`, the schema to explain\n- `:value` (optional), the value to explain\n- `:negated` (optional), a function returning the explanation of `(m/explain [:not schema] value)`.\n  If provided, then we are explaining the failure of negating this schema via `(m/explain [:not schema] value)`.\n  Note in this scenario, `(m/validate schema value)` is true.\n  If returning a string,\n  the resulting error message will be negated by the `:error/fn` caller in the same way as `:error/message`.\n  Returning `(negated string)` disables this behavior and `string` is used as the negated error message.\n```clojure\n;; automatic negation\n(me/humanize\n  (m/explain\n    [:not [:fn {:error/fn {:en (fn [_ _] \"should not be a multiple of 3\")}}\n           #(not= 0 (mod % 3))]]\n    1))\n; =\u003e [\"should be a multiple of 3\"]\n\n;; manual negation\n(me/humanize\n  (m/explain [:not [:fn {:error/fn {:en (fn [{:keys [negated]} _]\n                                          (if negated\n                                            (negated \"should not avoid being a multiple of 3\")\n                                            \"should not be a multiple of 3\"))}}\n                    #(not= 0 (mod % 3))]] 1))\n; =\u003e [\"should not avoid being a multiple of 3\"]\n```\n\nHere are some basic examples of `:error/message` and `:error/fn`:\n\n```clojure\n(-\u003e [:map\n     [:id int?]\n     [:size [:enum {:error/message \"should be: S|M|L\"}\n             \"S\" \"M\" \"L\"]]\n     [:age [:fn {:error/fn (fn [{:keys [value]} _] (str value \", should be \u003e 18\"))}\n            (fn [x] (and (int? x) (\u003e x 18)))]]]\n    (m/explain {:size \"XL\", :age 10})\n    (me/humanize\n      {:errors (-\u003e me/default-errors\n                   (assoc ::m/missing-key {:error/fn (fn [{:keys [in]} _] (str \"missing key \" (last in)))}))}))\n;{:id [\"missing key :id\"]\n; :size [\"should be: S|M|L\"]\n; :age [\"10, should be \u003e 18\"]}\n```\n\nMessages can be localized:\n\n```clojure\n(-\u003e [:map\n     [:id int?]\n     [:size [:enum {:error/message {:en \"should be: S|M|L\"\n                                    :fi \"pitäisi olla: S|M|L\"}}\n             \"S\" \"M\" \"L\"]]\n     [:age [:fn {:error/fn {:en (fn [{:keys [value]} _] (str value \", should be \u003e 18\"))\n                            :fi (fn [{:keys [value]} _] (str value \", pitäisi olla \u003e 18\"))}}\n            (fn [x] (and (int? x) (\u003e x 18)))]]]\n    (m/explain {:size \"XL\", :age 10})\n    (me/humanize\n      {:locale :fi\n       :errors (-\u003e me/default-errors\n                   (assoc-in ['int? :error-message :fi] \"pitäisi olla numero\")\n                   (assoc ::m/missing-key {:error/fn {:en (fn [{:keys [in]} _] (str \"missing key \" (last in)))\n                                                      :fi (fn [{:keys [in]} _] (str \"puuttuu avain \" (last in)))}}))}))\n;{:id [\"puuttuu avain :id\"]\n; :size [\"pitäisi olla: S|M|L\"]\n; :age [\"10, pitäisi olla \u003e 18\"]}\n```\n\nTop-level humanized map-errors are under `:malli/error`:\n\n```clojure\n(-\u003e [:and [:map\n           [:password string?]\n           [:password2 string?]]\n     [:fn {:error/message \"passwords don't match\"}\n       (fn [{:keys [password password2]}]\n         (= password password2))]]\n    (m/explain {:password \"secret\"\n                :password2 \"faarao\"})\n    (me/humanize))\n; =\u003e [\"passwords don't match\"]\n```\n\nErrors can be targeted using `:error/path` property:\n\n```clojure\n(-\u003e [:and [:map\n           [:password string?]\n           [:password2 string?]]\n     [:fn {:error/message \"passwords don't match\"\n           :error/path [:password2]}\n       (fn [{:keys [password password2]}]\n         (= password password2))]]\n    (m/explain {:password \"secret\"\n                :password2 \"faarao\"})\n    (me/humanize))\n; {:password2 [\"passwords don't match\"]}\n```\n\nBy default, only direct erroneous schema properties are used:\n\n```clojure\n(-\u003e [:map\n     [:foo {:error/message \"entry-failure\"} :int]] ;; here, :int fails, no error props\n    (m/explain {:foo \"1\"})\n    (me/humanize))\n; =\u003e {:foo [\"should be an integer\"]}\n```\n\nLooking up humanized errors from parent schemas with custom `:resolve` (BETA, subject to change):\n\n```clojure\n(-\u003e [:map\n     [:foo {:error/message \"entry-failure\"} :int]]\n    (m/explain {:foo \"1\"})\n    (me/humanize {:resolve me/-resolve-root-error}))\n; =\u003e {:foo [\"entry-failure\"]}\n```\n\n## Spell checking\n\nFor closed schemas, key spelling can be checked with:\n\n```clojure\n(-\u003e [:map [:address [:map [:street string?]]]]\n    (mu/closed-schema)\n    (m/explain\n      {:name \"Lie-mi\"\n       :address {:streetz \"Hämeenkatu 14\"}})\n    (me/with-spell-checking)\n    (me/humanize))\n;{:address {:streetz [\"should be spelled :street\"]}\n; :name [\"disallowed key\"]}\n```\n\n## Values in error\n\nJust to get parts of the value that are in error:\n\n```clojure\n(-\u003e Address\n    (m/explain\n     {:id \"Lillan\"\n      :tags #{:artesan \"coffee\" :garden \"ground\"}\n      :address {:street \"Ahlmanintie 29\"\n                :zip 33100\n                :lonlat [61.4858322, \"23.7832851,17\"]}})\n    (me/error-value))\n;{:tags #{\"coffee\" \"ground\"}\n; :address {:lonlat [nil \"23.7832851,17\"]}}\n```\n\nMasking irrelevant parts:\n\n```clojure\n(-\u003e Address\n    (m/explain\n     {:id \"Lillan\"\n      :tags #{:artesan \"coffee\" :garden \"ground\"}\n      :address {:street \"Ahlmanintie 29\"\n                :zip 33100\n                :lonlat [61.4858322, \"23.7832851,17\"]}})\n    (me/error-value {::me/mask-valid-values '...}))\n;{:id ...\n; :tags #{\"coffee\" \"ground\" ...}\n; :address {:street ...\n;           :zip ...\n;           :lonlat [... \"23.7832851,17\"]}}\n```\n\n## Pretty errors\n\nThere are two ways to get pretty errors:\n\n### Development mode\n\nStart development mode:\n\n```clojure\n((requiring-resolve 'malli.dev/start!))\n```\n\nNow, any exception thrown via `malli.core/-fail!` is being captured and pretty printed before being thrown. Pretty printing is extendable using [virhe](https://github.com/metosin/virhe).\n\nPretty Coercion:\n\n\u003cimg src=\"docs/img/pretty-coerce.png\" width=800\u003e\n\nCustom exception (with default layout):\n\n\u003cimg src=\"docs/img/bats-in-the-attic.png\" width=800\u003e\n\nPretty printing in being backed by `malli.dev.virhe/-format` multimethod using `(-\u003e 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:\n\n```clojure\n(require '[malli.dev.virhe :as v])\n\n(defmethod v/-format java.sql.SQLException [e _ printer]\n  {:title \"Exception thrown\"\n   :body [:group\n          (v/-block \"SQL Exception\" (v/-color :string (ex-message e) printer) printer) :break :break\n          (v/-block \"More information:\" (v/-link \"https://cljdoc.org/d/metosin/malli/CURRENT\" printer) printer)]})\n```\n\n### pretty/explain\n\nFor pretty development-time error printing, try `malli.dev.pretty/explain`\n\n\u003cimg src=\"docs/img/pretty-explain.png\" width=800\u003e\n\n## Value transformation\n\n```clojure\n(require '[malli.transform :as mt])\n```\n\nTwo-way schema-driven value transformations with `m/decode` and `m/encode` using a `Transformer` instance.\n\nDefault Transformers include:\n\n| name                              | description                                         |\n|:----------------------------------|-----------------------------------------------------|\n| `mt/string-transformer`           | transform between strings and EDN                   |\n| `mt/json-transformer`             | transform between JSON and EDN                      |\n| `mt/strip-extra-keys-transformer` | drop extra keys from maps                           |\n| `mt/default-value-transformer`    | applies default values from schema properties       |\n| `mt/key-transformer`              | transforms map keys                                |\n| `mt/collection-transformer`       | conversion between collections (e.g. set -\u003e vector) |\n\n**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.\n\nSimple usage:\n\n```clojure\n(m/decode int? \"42\" mt/string-transformer)\n; 42\n\n(m/encode int? 42 mt/string-transformer)\n; \"42\"\n```\n\nFor performance, precompute the transformations with `m/decoder` and `m/encoder`:\n\n```clojure\n(def decode (m/decoder int? mt/string-transformer))\n\n(decode \"42\")\n; 42\n\n(def encode (m/encoder int? mt/string-transformer))\n\n(encode 42)\n; \"42\"\n```\n\n### Coercion\n\nFor both decoding + validating the results (throwing exception on error), there is `m/coerce` and `m/coercer`:\n\n```clojure\n(m/coerce :int \"42\" mt/string-transformer)\n; 42\n\n((m/coercer :int mt/string-transformer) \"42\")\n; 42\n\n(m/coerce :int \"invalid\" mt/string-transformer)\n; =throws=\u003e :malli.core/invalid-input {:value \"invalid\", :schema :int, :explain {:schema :int, :value \"invalid\", :errors ({:path [], :in [], :schema :int, :value \"invalid\"})}}\n```\n\nCoercion can be applied without transformer, doing just validation:\n\n```clojure\n(m/coerce :int 42)\n; 42\n\n(m/coerce :int \"42\")\n; =throws=\u003e :malli.core/invalid-input {:value \"42\", :schema :int, :explain {:schema :int, :value \"42\", :errors ({:path [], :in [], :schema :int, :value \"42\"})}}\n```\n\nException-free coercion with continuation-passing style:\n\n```clojure\n(m/coerce :int \"fail\" nil (partial prn \"success:\") (partial prn \"error:\"))\n; =prints=\u003e \"error:\" {:value \"fail\", :schema :int, :explain ...}\n```\n\n### Advanced Transformations\n\nTransformations are recursive:\n\n```clojure\n(m/decode\n  Address\n  {:id \"Lillan\",\n   :tags [\"coffee\" \"artesan\" \"garden\"],\n   :address {:street \"Ahlmanintie 29\"\n             :city \"Tampere\"\n             :zip 33100\n             :lonlat [61.4858322 23.7854658]}}\n  mt/json-transformer)\n;{:id \"Lillan\",\n; :tags #{:coffee :artesan :garden},\n; :address {:street \"Ahlmanintie 29\"\n;           :city \"Tampere\"\n;           :zip 33100\n;           :lonlat [61.4858322 23.7854658]}}\n```\n\nTransform map keys:\n\n```clojure\n(m/encode\n  Address\n  {:id \"Lillan\",\n   :tags [\"coffee\" \"artesan\" \"garden\"],\n   :address {:street \"Ahlmanintie 29\"\n             :city \"Tampere\"\n             :zip 33100\n             :lonlat [61.4858322 23.7854658]}}\n  (mt/key-transformer {:encode name}))\n;{\"id\" \"Lillan\",\n; \"tags\" [\"coffee\" \"artesan\" \"garden\"],\n; \"address\" {\"street\" \"Ahlmanintie 29\"\n;            \"city\" \"Tampere\"\n;            \"zip\" 33100\n;            \"lonlat\" [61.4858322 23.7854658]}}\n```\n\nTransforming homogenous `:enum` or `:=`s (supports automatic type detection of `:keyword`, `:symbol`, `:int` and `:double`):\n\n```clojure\n(m/decode [:enum :kikka :kukka] \"kukka\" mt/string-transformer)\n; =\u003e :kukka\n```\n\nTransformers can be composed with `mt/transformer`:\n\n```clojure\n(def strict-json-transformer\n  (mt/transformer\n    mt/strip-extra-keys-transformer\n    mt/json-transformer))\n\n(m/decode\n  Address\n  {:id \"Lillan\",\n   :EVIL \"LYN\"\n   :tags [\"coffee\" \"artesan\" \"garden\"],\n   :address {:street \"Ahlmanintie 29\"\n             :DARK \"ORKO\"\n             :city \"Tampere\"\n             :zip 33100\n             :lonlat [61.4858322 23.7854658]}}\n  strict-json-transformer)\n;{:id \"Lillan\",\n; :tags #{:coffee :artesan :garden},\n; :address {:street \"Ahlmanintie 29\"\n;           :city \"Tampere\"\n;           :zip 33100\n;           :lonlat [61.4858322 23.7854658]}}\n```\n\nSchema properties can be used to override default transformations:\n\n```clojure\n(m/decode\n  [string? {:decode/string clojure.string/upper-case}]\n  \"kerran\" mt/string-transformer)\n; =\u003e \"KERRAN\"\n```\n\nThis works too:\n\n```clojure\n(m/decode\n  [string? {:decode {:string clojure.string/upper-case}}]\n  \"kerran\" mt/string-transformer)\n; =\u003e \"KERRAN\"\n```\n\nDecoders and encoders as interceptors (with `:enter` and `:leave` stages):\n\n```clojure\n(m/decode\n  [string? {:decode/string {:enter clojure.string/upper-case}}]\n  \"kerran\" mt/string-transformer)\n; =\u003e \"KERRAN\"\n```\n\n```clojure\n(m/decode\n  [string? {:decode/string {:enter #(str \"olipa_\" %)\n                            :leave #(str % \"_avaruus\")}}]\n  \"kerran\" mt/string-transformer)\n; =\u003e \"olipa_kerran_avaruus\"\n```\n\nTo access Schema (and options) use `:compile`:\n\n```clojure\n(m/decode\n  [int? {:math/multiplier 10\n         :decode/math {:compile (fn [schema _]\n                                  (let [multiplier (:math/multiplier (m/properties schema))]\n                                    (fn [x] (* x multiplier))))}}]\n  12\n  (mt/transformer {:name :math}))\n; =\u003e 120\n```\n\nGoing crazy:\n\n```clojure\n(m/decode\n  [:map\n   {:decode/math {:enter #(update % :x inc)\n                  :leave #(update % :x (partial * 2))}}\n   [:x [int? {:decode/math {:enter (partial + 2)\n                            :leave (partial * 3)}}]]]\n  {:x 1}\n  (mt/transformer {:name :math}))\n; =\u003e {:x 24}\n```\n\n`:and` accumulates the transformed value left-to-right.\n\n```clojure\n(m/decode\n  [:and\n   [:string {:decode/string '{:enter #(str \"1_\" %), :leave #(str % \"_2\")}}]\n   [:string {:decode/string '{:enter #(str \"3_\" %), :leave #(str % \"_4\")}}]]\n  \"kerran\" mt/string-transformer)\n;; =\u003e \"3_1_kerran_2_4\"\n```\n\n`:or` transforms using the first successful schema, left-to-right.\n\n```clojure\n(m/decode\n  [:or\n   [:string {:decode/string '{:enter #(str \"1_\" %), :leave #(str % \"_2\")}}]\n   [:string {:decode/string '{:enter #(str \"3_\" %), :leave #(str % \"_4\")}}]]\n  \"kerran\" mt/string-transformer)\n;; =\u003e \"1_kerran_2\"\n\n(m/decode\n  [:or\n   :map\n   [:string {:decode/string '{:enter #(str \"3_\" %), :leave #(str % \"_4\")}}]]\n  \"kerran\" mt/string-transformer)\n;; =\u003e \"3_kerran_4\"\n```\n\nProxy schemas like `:merge` and `:union` transform as if `m/deref`ed.\n\n```clojure\n(m/decode\n  [:merge\n   [:map [:name [:string {:default \"kikka\"}]] ]\n   [:map [:description {:optional true} [:string {:default \"kikka\"}]]]]\n  {}\n  {:registry (merge (mu/schemas) (m/default-schemas))}\n  (mt/default-value-transformer {::mt/add-optional-keys true}))\n;; =\u003e {:name \"kikka\"\n;;     :description \"kikka\"}\n```\n\n## To and from JSON\n\nThe `m/encode` and `m/decode` functions work on clojure data. To go\nfrom clojure data to JSON, you need a JSON library like\n[jsonista](https://github.com/metosin/jsonista). Additionally, since\n`m/decode` doesn't check the schema, you need to run `m/validate` (or\n`m/explain`) if you want to make sure your data conforms to your\nschema.\n\nTo JSON:\n\n```clojure\n(def Tags\n  (m/schema [:map\n             {:closed true}\n             [:tags [:set :keyword]]]))\n(jsonista.core/write-value-as-string\n (m/encode Tags\n           {:tags #{:bar :quux}}\n           mt/json-transformer))\n; =\u003e \"{\\\"tags\\\":[\\\"bar\\\",\\\"quux\\\"]}\"\n```\n\nFrom JSON without validation:\n\n```clojure\n(m/decode Tags\n          (jsonista.core/read-value \"{\\\"tags\\\":[\\\"bar\\\",[\\\"quux\\\"]]}\"\n                                    jsonista.core/keyword-keys-object-mapper)\n          mt/json-transformer)\n; =\u003e {:tags #{:bar [\"quux\"]}}\n```\n\nFrom JSON with validation:\n\n```clojure\n(m/explain Tags\n           (m/decode Tags\n                     (jsonista.core/read-value \"{\\\"tags\\\":[\\\"bar\\\",[\\\"quux\\\"]]}\"\n                                               jsonista.core/keyword-keys-object-mapper)\n                     mt/json-transformer))\n; =\u003e {:schema [:map {:closed true} [:tags [:set :keyword]]],\n;     :value {:tags #{:bar [\"quux\"]}},\n;     :errors ({:path [:tags 0], :in [:tags [\"quux\"]], :schema :keyword, :value [\"quux\"]})}\n```\n\n```clojure\n(m/validate Tags\n            (m/decode Tags\n                      (jsonista.core/read-value \"{\\\"tags\\\":[\\\"bar\\\",\\\"quux\\\"]}\" ; \u003c- note! no error\n                                                jsonista.core/keyword-keys-object-mapper)\n                      mt/json-transformer))\n; =\u003e true\n```\n\nFor performance, it's best to prebuild the validator, decoder and explainer:\n\n```clojure\n(def validate-Tags (m/validator Tags))\n(def decode-Tags (m/decoder Tags mt/json-transformer))\n(-\u003e (jsonista.core/read-value \"{\\\"tags\\\":[\\\"bar\\\",\\\"quux\\\"]}\"\n                              jsonista.core/keyword-keys-object-mapper)\n    decode-Tags\n    validate-Tags)\n; =\u003e true\n```\n\n## Default values\n\nApplying default values:\n\n```clojure\n(m/decode [:and {:default 42} int?] nil mt/default-value-transformer)\n; =\u003e 42\n```\n\nWith custom key and type defaults:\n\n```clojure\n(m/decode\n  [:map\n   [:user [:map\n           [:name :string]\n           [:description {:ui/default \"-\"} :string]]]]\n  nil\n  (mt/default-value-transformer\n    {:key :ui/default\n     :defaults {:map (constantly {})\n                :string (constantly \"\")}}))\n; =\u003e {:user {:name \"\", :description \"-\"}}\n```\n\nWith custom function:\n\n```clojure\n(m/decode\n [:map\n  [:os [:string {:property \"os.name\"}]]\n  [:timezone [:string {:property \"user.timezone\"}]]]\n {}\n (mt/default-value-transformer\n  {:key :property\n   :default-fn (fn [_ x] (System/getProperty x))}))\n; =\u003e {:os \"Mac OS X\", :timezone \"Europe/Helsinki\"}\n```\n\nOptional Keys are not added by default:\n\n```clojure\n(m/decode\n [:map\n  [:name [:string {:default \"kikka\"}]]\n  [:description {:optional true} [:string {:default \"kikka\"}]]]\n {}\n (mt/default-value-transformer))\n; =\u003e {:name \"kikka\"}\n```\n\nAdding optional keys too via `::mt/add-optional-keys` option:\n\n```clojure\n(m/decode\n [:map\n  [:name [:string {:default \"kikka\"}]]\n  [:description {:optional true} [:string {:default \"kikka\"}]]]\n {}\n (mt/default-value-transformer {::mt/add-optional-keys true}))\n; =\u003e {:name \"kikka\", :description \"kikka\"}\n```\n\nSingle sweep of defaults \u0026 string encoding:\n\n```clojure\n(m/encode\n  [:map {:default {}}\n   [:a [int? {:default 1}]]\n   [:b [:vector {:default [1 2 3]} int?]]\n   [:c [:map {:default {}}\n        [:x [int? {:default 42}]]\n        [:y int?]]]\n   [:d [:map\n        [:x [int? {:default 42}]]\n        [:y int?]]]\n   [:e int?]]\n  nil\n  (mt/transformer\n    mt/default-value-transformer\n    mt/string-transformer))\n;{:a \"1\"\n; :b [\"1\" \"2\" \"3\"]\n; :c {:x \"42\"}}\n```\n\n## Programming with schemas\n\n```clojure\n(require '[malli.util :as mu])\n```\n\nUpdating Schema properties:\n\n```clojure\n(mu/update-properties [:vector int?] assoc :min 1)\n; =\u003e [:vector {:min 1} int?]\n```\n\nLifted `clojure.core` function to work with schemas: `select-keys`, `dissoc`, `get`, `assoc`, `update`, `get-in`, `assoc-in`, `update-in`\n\n```clojure\n(mu/get-in Address [:address :lonlat])\n; =\u003e [:tuple double? double?]\n\n(mu/update-in Address [:address] mu/assoc :country [:enum \"fi\" \"po\"])\n;[:map\n; [:id string?]\n; [:tags [:set keyword?]]\n; [:address\n;  [:map [:street string?]\n;   [:city string?]\n;   [:zip int?]\n;   [:lonlat [:tuple double? double?]]\n;   [:country [:enum \"fi\" \"po\"]]]]]\n\n(-\u003e Address\n    (mu/dissoc :address)\n    (mu/update-properties assoc :title \"Address\"))\n;[:map {:title \"Address\"}\n; [:id string?]\n; [:tags [:set keyword?]]]\n```\n\nMaking keys optional or required:\n\n```clojure\n(mu/optional-keys [:map [:x int?] [:y int?]])\n;[:map\n; [:x {:optional true} int?]\n; [:y {:optional true} int?]]\n\n(mu/optional-keys [:map [:x int?] [:y int?]]\n                  [:x])\n;[:map\n; [:x {:optional true} int?]\n; [:y int?]]\n\n(mu/required-keys [:map [:x {:optional true} int?] [:y {:optional true} int?]])\n;[:map\n; [:x int?]\n; [:y int?]]\n\n(mu/required-keys [:map [:x {:optional true} int?] [:y {:optional true} int?]]\n                  [:x])\n;[:map\n; [:x int?]\n; [:y {:optional true} int?]]\n```\n\nClosing and opening all `:map` schemas recursively:\n\n```clojure\n(def abcd\n  [:map {:title \"abcd\"}\n   [:a int?]\n   [:b {:optional true} int?]\n   [:c [:map\n        [:d int?]]]])\n\n(mu/closed-schema abcd)\n;[:map {:title \"abcd\", :closed true}\n; [:a int?]\n; [:b {:optional true} int?]\n; [:c [:map {:closed true}\n;      [:d int?]]]]\n\n(-\u003e abcd\n    mu/closed-schema\n    mu/open-schema)\n;[:map {:title \"abcd\"}\n; [:a int?]\n; [:b {:optional true} int?]\n; [:c [:map\n;      [:d int?]]]]\n```\n\nMerging Schemas (last value wins):\n\n```clojure\n(mu/merge\n  [:map\n   [:name string?]\n   [:description string?]\n   [:address\n    [:map\n     [:street string?]\n     [:country [:enum \"finland\" \"poland\"]]]]]\n  [:map\n   [:description {:optional true} string?]\n   [:address\n    [:map\n     [:country string?]]]])\n;[:map\n; [:name string?]\n; [:description {:optional true} string?]\n; [:address [:map\n;            [:street string?]\n;            [:country string?]]]]\n```\n\nWith `:and`, first child is used in merge:\n\n```clojure\n(mu/merge\n  [:and {:type \"entity\"}\n   [:map {:title \"user\"}\n    [:name :string]]\n   map?]\n  [:map {:description \"aged\"} [:age :int]])\n;[:and {:type \"entity\"}\n; [:map {:title \"user\", :description \"aged\"}\n;  [:name :string]\n;  [:age :int]]\n; map?]\n```\n\nSchema unions (merged values of both schemas are valid for union schema):\n\n```clojure\n(mu/union\n  [:map\n   [:name string?]\n   [:description string?]\n   [:address\n    [:map\n     [:street string?]\n     [:country [:enum \"finland\" \"poland\"]]]]]\n  [:map\n   [:description {:optional true} string?]\n   [:address\n    [:map\n     [:country string?]]]])\n;[:map\n; [:name string?]\n; [:description {:optional true} string?]\n; [:address [:map\n;            [:street string?]\n;            [:country [:or [:enum \"finland\" \"poland\"] string?]]]]]\n```\n\nAdding generated example values to Schemas:\n\n```clojure\n(m/walk\n  [:map\n   [:name string?]\n   [:description string?]\n   [:address\n    [:map\n     [:street string?]\n     [:country [:enum \"finland\" \"poland\"]]]]]\n  (m/schema-walker\n    (fn [schema]\n      (mu/update-properties schema assoc :examples (mg/sample schema {:size 2, :seed 20})))))\n;[:map\n; {:examples ({:name \"\", :description \"\", :address {:street \"\", :country \"poland\"}}\n;             {:name \"W\", :description \"x\", :address {:street \"8\", :country \"finland\"}})}\n; [:name [string? {:examples (\"\" \"\")}]]\n; [:description [string? {:examples (\"\" \"\")}]]\n; [:address\n;  [:map\n;   {:examples ({:street \"\", :country \"finland\"} {:street \"W\", :country \"poland\"})}\n;   [:street [string? {:examples (\"\" \"\")}]]\n;   [:country [:enum {:examples (\"finland\" \"poland\")} \"finland\" \"poland\"]]]]]\n```\n\nFinding first value (prewalk):\n\n```clojure\n(mu/find-first\n  [:map\n   [:x int?]\n   [:y [:vector [:tuple\n                 [:or [:and {:salaisuus \"turvassa\"} boolean?] int?]\n                 [:schema {:salaisuus \"vaarassa\"} false?]]]]\n   [:z [:string {:salaisuus \"piilossa\"}]]]\n  (fn [schema _ _]\n    (-\u003e schema m/properties :salaisuus)))\n; =\u003e \"turvassa\"\n```\n\nFinding all subschemas with paths, retaining order:\n\n```clojure\n(def Schema\n  (m/schema\n    [:maybe\n     [:map\n      [:id string?]\n      [:tags [:set keyword?]]\n      [:address\n       [:and\n        [:map\n         [:street {:optional true} string?]\n         [:lonlat {:optional true} [:tuple double? double?]]]\n        [:fn (fn [{:keys [street lonlat]}] (or street lonlat))]]]]]))\n\n(mu/subschemas Schema)\n;[{:path [], :in [], :schema [:maybe\n;                             [:map\n;                              [:id string?]\n;                              [:tags [:set keyword?]]\n;                              [:address\n;                               [:and\n;                                [:map\n;                                 [:street {:optional true} string?]\n;                                 [:lonlat {:optional true} [:tuple double? double?]]]\n;                                [:fn (fn [{:keys [street lonlat]}] (or street lonlat))]]]]]}\n; {:path [0], :in [], :schema [:map\n;                              [:id string?]\n;                              [:tags [:set keyword?]]\n;                              [:address\n;                               [:and\n;                                [:map\n;                                 [:street {:optional true} string?]\n;                                 [:lonlat {:optional true} [:tuple double? double?]]]\n;                                [:fn (fn [{:keys [street lonlat]}] (or street lonlat))]]]]}\n; {:path [0 :id], :in [:id], :schema string?}\n; {:path [0 :tags], :in [:tags], :schema [:set keyword?]}\n; {:path [0 :tags :malli.core/in], :in [:tags :malli.core/in], :schema keyword?}\n; {:path [0 :address], :in [:address], :schema [:and\n;                                               [:map\n;                                                [:street {:optional true} string?]\n;                                                [:lonlat {:optional true} [:tuple double? double?]]]\n;                                               [:fn (fn [{:keys [street lonlat]}] (or street lonlat))]]}\n; {:path [0 :address 0], :in [:address], :schema [:map\n;                                                 [:street {:optional true} string?]\n;                                                 [:lonlat {:optional true} [:tuple double? double?]]]}\n; {:path [0 :address 0 :street], :in [:address :street], :schema string?}\n; {:path [0 :address 0 :lonlat], :in [:address :lonlat], :schema [:tuple double? double?]}\n; {:path [0 :address 0 :lonlat 0], :in [:address :lonlat 0], :schema double?}\n; {:path [0 :address 0 :lonlat 1], :in [:address :lonlat 1], :schema double?}\n; {:path [0 :address 1], :in [:address], :schema [:fn (fn [{:keys [street lonlat]}] (or street lonlat))]}]\n```\n\nCollecting unique value paths and their schema paths:\n\n```clojure\n(-\u003e\u003e Schema\n     (mu/subschemas)\n     (mu/distinct-by :id)\n     (mapv (juxt :in :path)))\n;[[[] []]\n; [[] [0]]\n; [[:id] [0 :id]]\n; [[:tags] [0 :tags]]\n; [[:tags :malli.core/in] [0 :tags :malli.core/in]]\n; [[:address] [0 :address]]\n; [[:address] [0 :address 0]]\n; [[:address :street] [0 :address 0 :street]]\n; [[:address :lonlat] [0 :address 0 :lonlat]]\n; [[:address :lonlat 0] [0 :address 0 :lonlat 0]]\n; [[:address :lonlat 1] [0 :address 0 :lonlat 1]]\n; [[:address] [0 :address 1]]]\n```\n\nSchema paths can be converted into value paths:\n\n```clojure\n(mu/get-in Schema [0 :address 0 :lonlat])\n; =\u003e [:tuple double? double?]\n\n(mu/path-\u003ein Schema [0 :address 0 :lonlat])\n; =\u003e [:address :lonlat]\n```\n\nand back, returning all paths:\n\n```clojure\n(mu/in-\u003epaths Schema [:address :lonlat])\n; =\u003e [[0 :address 0 :lonlat]]\n```\n\n## Declarative schema transformation\n\nThere are also declarative versions of schema transforming utilities in `malli.util/schemas`. These include `:merge`, `:union` and `:select-keys`:\n\n```clojure\n(def registry (merge (m/default-schemas) (mu/schemas)))\n\n(def Merged\n  (m/schema\n    [:merge\n     [:map [:x :string]]\n     [:map [:y :int]]]\n    {:registry registry}))\n\nMerged\n;[:merge\n; [:map [:x :string]]\n; [:map [:y :int]]]\n\n(m/deref Merged)\n;[:map\n; [:x :string]\n; [:y :int]]\n\n(m/validate Merged {:x \"kikka\", :y 6})\n; =\u003e true\n```\n\n`:union` is similar to `:or`, except `:union` combines map schemas in different disjuncts with `:or`.\nFor example, `UnionMaps` is equivalent to `[:map [:x [:or :int :string]] [:y [:or :int :string]]]`.\n\n```clojure\n(def OrMaps\n  (m/schema\n    [:or\n     [:map [:x :int] [:y :string]]\n     [:map [:x :string] [:y :int]]]\n    {:registry registry}))\n\n(def UnionMaps\n  (m/schema\n    [:union\n     [:map [:x :int] [:y :string]]\n     [:map [:x :string] [:y :int]]]\n    {:registry registry}))\n\n(m/validate OrMaps {:x \"kikka\" :y \"kikka\"})\n; =\u003e false\n\n(m/validate UnionMaps {:x \"kikka\" :y \"kikka\"})\n; =\u003e true\n```\n\n`:merge` and `:union` differ on schemas with common keys. `:merge` chooses the right-most\nschema of common keys, and `:union` combines them with `:or`.\nFor example, `MergedCommon` is equivalent to `[:map [:x :int]]`, and `UnionCommon`\nis equivalent to `[:map [:x [:or :string :int]]]`.\n\n```clojure\n(def MergedCommon\n  (m/schema\n    [:merge\n     [:map [:x :string]]\n     [:map [:x :int]]]\n    {:registry registry}))\n\n(def UnionCommon\n  (m/schema\n    [:union\n     [:map [:x :string]]\n     [:map [:x :int]]]\n    {:registry registry}))\n\n(m/validate MergedCommon {:x \"kikka\"})\n; =\u003e false\n(m/validate MergedCommon {:x 1})\n; =\u003e true\n(m/validate UnionCommon {:x \"kikka\"})\n; =\u003e true\n(m/validate UnionCommon {:x 1})\n; =\u003e true\n```\n\n### Distributive schemas\n\n`:merge` also distributes over `:multi` in a [similar way](https://en.wikipedia.org/wiki/Distributive_property) to how multiplication\ndistributes over addition in arithmetic. There are two transformation rules, applied in the following order:\n\n```clojure\n;; right-distributive\n[:merge [:multi M1 M2 ...] M3]\n=\u003e\n[:multi [:merge M1 M3] [:merge M2 M3] ...]\n\n;; left-distributive\n[:merge M1 [:multi M2 M3 ...]]\n=\u003e\n[:multi [:merge M1 M2] [:merge M1 M3] ...]\n```\n\nFor `:merge` with more than two arguments, the rules are applied iteratively left-to-right\nas if the following transformation was applied:\n\n```clojure\n[:merge M1 M2 M3 M4 ...]\n=\u003e\n[:merge\n [:merge\n  [:merge M1 M2]\n  M3]\n M4]\n...\n```\n\nThe distributive property of `:multi` is useful combined with `:merge`\nif you want all clauses of a `:multi` to share extra entries.\n\nHere are concrete examples of applying the rules:\n\n```clojure\n;; left-distributive\n(m/deref\n [:merge\n  [:map [:x :int]]\n  [:multi {:dispatch :y}\n   [1 [:map [:y [:= 1]]]]\n   [2 [:map [:y [:= 2]]]]]]\n {:registry registry})\n; =\u003e [:multi {:dispatch :y}\n;     [1 [:map [:x :int] [:y [:= 1]]]]\n;     [2 [:map [:x :int] [:y [:= 2]]]]]\n\n;; right-distributive\n(m/deref\n [:merge\n  [:multi {:dispatch :y}\n   [1 [:map [:y [:= 1]]]]\n   [2 [:map [:y [:= 2]]]]]\n  [:map [:x :int]]]\n {:registry registry})\n; =\u003e [:multi {:dispatch :y}\n;     [1 [:map [:y [:= 1]] [:x :int]]]\n;     [2 [:map [:y [:= 2]] [:x :int]]]]\n```\n\nIt is not recommended to use local registries in schemas that are transformed.\nAlso be aware that merging non-maps via the distributive property inherits\nthe same semantics as `:merge`, which is based on [meta-merge](https://github.com/weavejester/meta-merge).\n\n## Persisting schemas\n\nWriting and Reading schemas as [EDN](https://github.com/edn-format/edn), no `eval` needed.\n\nFollowing example requires [SCI](https://github.com/babashka/sci) or\n[cherry](https://github.com/squint-cljs/cherry) as external dependency because\nit includes a (quoted) function definition. See [Serializable\nfunctions](#serializable-functions).\n\n```clojure\n(require '[malli.edn :as edn])\n\n(-\u003e [:and\n     [:map\n      [:x int?]\n      [:y int?]]\n     [:fn '(fn [{:keys [x y]}] (\u003e x y))]]\n    (edn/write-string)\n    (doto prn) ; =\u003e \"[:and [:map [:x int?] [:y int?]] [:fn (fn [{:keys [x y]}] (\u003e x y))]]\"\n    (edn/read-string)\n    (doto (-\u003e (m/validate {:x 0, :y 1}) prn)) ; =\u003e false\n    (doto (-\u003e (m/validate {:x 2, :y 1}) prn))) ; =\u003e true\n;[:and\n; [:map\n;  [:x int?]\n;  [:y int?]]\n; [:fn (fn [{:keys [x y]}] (\u003e x y))]]\n```\n\n## Multi schemas\n\nClosed dispatch with `:multi` schema and `:dispatch` property:\n\n```clojure\n(m/validate\n  [:multi {:dispatch :type}\n   [:sized [:map [:type keyword?] [:size int?]]]\n   [:human [:map [:type keyword?] [:name string?] [:address [:map [:country keyword?]]]]]]\n  {:type :sized, :size 10})\n; true\n```\n\nDefault branch with `::m/default`:\n\n```clojure\n(def valid?\n  (m/validator\n    [:multi {:dispatch :type}\n     [\"object\" [:map-of :keyword :string]]\n     [::m/default :string]]))\n\n(valid? {:type \"object\", :key \"1\", :value \"100\"})\n; =\u003e true\n\n(valid? \"SUCCESS!\")\n; =\u003e true\n\n(valid? :failure)\n; =\u003e false\n```\n\nAny function can be used for `:dispatch`:\n\n```clojure\n(m/validate\n  [:multi {:dispatch first}\n   [:sized [:tuple keyword? [:map [:size int?]]]]\n   [:human [:tuple keyword? [:map [:name string?] [:address [:map [:country keyword?]]]]]]]\n  [:human {:name \"seppo\", :address {:country :sweden}}])\n; true\n```\n\n`:dispatch` values should be decoded before actual values:\n\n```clojure\n(m/decode\n  [:multi {:dispatch :type\n           :decode/string #(update % :type keyword)}\n   [:sized [:map [:type [:= :sized]] [:size int?]]]\n   [:human [:map [:type [:= :human]] [:name string?] [:address [:map [:country keyword?]]]]]]\n  {:type \"human\"\n   :name \"Tiina\"\n   :age \"98\"\n   :address {:country \"finland\"\n             :street \"this is an extra key\"}}\n  (mt/transformer mt/strip-extra-keys-transformer mt/string-transformer))\n;{:type :human\n; :name \"Tiina\"\n; :address {:country :finland}}\n```\n\n## Recursive schemas\n\nTo create a recursive schema, introduce a [local registry](#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.\n\nFor example, here is a recursive schema using `:schema` for singly-linked lists of positive integers:\n\n```clojure\n(m/validate\n  [:schema {:registry {::cons [:maybe [:tuple pos-int? [:ref ::cons]]]}}\n   [:ref ::cons]]\n  [16 [64 [26 [1 [13 nil]]]]])\n; =\u003e true\n```\n\nWithout the `:ref` keyword, malli eagerly expands the schema until a stack overflow error is thrown:\n\n```clojure\n(m/validate\n  [:schema {:registry {::cons [:maybe [:tuple pos-int? ::cons]]}}\n   ::cons]\n  [16 [64 [26 [1 [13 nil]]]]])\n; StackOverflowError\n```\n\nTechnically, you only need the `:ref` in recursive positions. However, it is best practice to `:ref` all references\nto recursive variables for better-behaving generators:\n\n```clojure\n;; Note:\n[:schema {:registry {::cons [:maybe [:tuple pos-int? [:ref ::cons]]]}}\n ::cons]\n;; produces the same generator as the \"unfolded\"\n[:maybe [:tuple pos-int? [:schema {:registry {::cons [:maybe [:tuple pos-int? [:ref ::cons]]]}} ::cons]]]\n;; while\n[:schema {:registry {::cons [:maybe [:tuple pos-int? [:ref ::cons]]]}}\n [:ref ::cons]]\n;; has a direct correspondance to the following generator:\n(gen/recursive-gen\n  (fn [rec] (gen/one-of [(gen/return nil) (gen/tuple rec)]))\n  (gen/return nil))\n```\n\n\nMutual 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:\n\n```clojure\n(m/validate\n  [:schema {:registry {::ping [:maybe [:tuple [:= \"ping\"] [:ref ::pong]]]\n                       ::pong [:maybe [:tuple [:= \"pong\"] [:ref ::ping]]]}}\n   ::ping]\n  [\"ping\" [\"pong\" [\"ping\" [\"pong\" [\"ping\" nil]]]]])\n; =\u003e true\n```\n\nNested registries, the last definition wins:\n\n```clojure\n(m/validate\n  [:schema {:registry {::ping [:maybe [:tuple [:= \"ping\"] [:ref ::pong]]]\n                       ::pong any?}} ;; effectively unreachable\n   [:schema {:registry {::pong [:maybe [:tuple [:= \"pong\"] [:ref ::ping]]]}}\n    ::ping]]\n  [\"ping\" [\"pong\" [\"ping\" [\"pong\" [\"ping\" nil]]]]])\n; =\u003e true\n```\n\n## Value generation\n\nSchemas can be used to generate values:\n\n```clojure\n(require '[malli.generator :as mg])\n\n;; random\n(mg/generate keyword?)\n; =\u003e :?\n\n;; using seed\n(mg/generate [:enum \"a\" \"b\" \"c\"] {:seed 42})\n;; =\u003e \"a\"\n\n;; using seed and size\n(mg/generate pos-int? {:seed 10, :size 100})\n;; =\u003e 55740\n\n;; regexs work too (only clj and if [com.gfredericks/test.chuck \"0.2.10\"+] available)\n(mg/generate\n  [:re #\"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,63}$\"]\n  {:seed 42, :size 10})\n; =\u003e \"CaR@MavCk70OHiX.yZ\"\n\n;; :gen/return (note, not validated)\n(mg/generate\n [:and {:gen/return 42} :int])\n; =\u003e 42\n\n;; :gen/elements (note, are not validated)\n(mg/generate\n  [:and {:gen/elements [\"kikka\" \"kukka\" \"kakka\"]} string?]\n  {:seed 10})\n; =\u003e \"kikka\"\n\n;; :gen/fmap\n(mg/generate\n  [:and {:gen/fmap (partial str \"kikka_\")} string?]\n  {:seed 10, :size 10})\n;; =\u003e \"kikka_WT3K0yax2\"\n\n;; portable :gen/fmap (requires `org.babashka/sci` dependency to work)\n(mg/generate\n  [:and {:gen/fmap '(partial str \"kikka_\")} string?]\n  {:seed 10, :size 10})\n;; =\u003e \"kikka_nWT3K0ya7\"\n\n;; :gen/schema\n(mg/generate\n  [:any {:gen/schema [:int {:min 10, :max 20}]}]\n  {:seed 10})\n; =\u003e 19\n\n;; :gen/min \u0026 :gen/max for numbers and collections\n(mg/generate\n  [:vector {:gen/min 4, :gen/max 4} :int]\n  {:seed 1})\n; =\u003e [-8522515 -1433 -1 1]\n\n;; :gen/infinite? \u0026 :gen/NaN? for :double\n(mg/generate\n  [:double {:gen/infinite? true, :gen/NaN? true}]\n  {:seed 1})\n; =\u003e ##Inf\n\n(require '[clojure.test.check.generators :as gen])\n\n;; gen/gen (note, not serializable)\n(mg/generate\n  [:sequential {:gen/gen (gen/list gen/neg-int)} int?]\n  {:size 42, :seed 42})\n; =\u003e (-37 -13 -13 -24 -20 -11 -34 -40 -22 0 -10)\n```\n\nGenerated values are valid:\n\n```clojure\n(mg/generate Address {:seed 123, :size 4})\n;{:id \"H7\",\n; :tags #{:v?.w.t6!.QJYk-/-?s*4\n;         :_7U\n;         :QdG/Xi8J\n;         :*Q-.p*8*/n-J9u}\n; :address {:street \"V9s\"\n;           :city \"\"\n;           :zip 3\n;           :lonlat [-2.75 -0.625]}}\n\n(m/validate Address (mg/generate Address))\n; =\u003e true\n```\n\nSampling values:\n\n```clojure\n;; sampling\n(mg/sample [:and int? [:\u003e 10] [:\u003c 100]] {:seed 123})\n; =\u003e (25 39 51 13 53 43 57 15 26 27)\n```\n\nIntegration with test.check:\n\n```clojure\n(require '[clojure.test.check.generators :as gen])\n(gen/sample (mg/generator pos-int?))\n; =\u003e (2 1 2 2 2 2 8 1 55 83)\n```\n\n### :and generation\n\nGenerators for `:and` schemas work by generating values from the first child, and then filtering\nout any values that do not pass the overall `:and` schema.\n\nFor the most reliable results, place the schema that is most likely to generate valid\nvalues for the entire schema as the first child of an `:and` schema.\n\n```clojure\n;; BAD: :string is unlikely to generate values satisfying the schema\n(mg/generate [:and :string [:enum \"a\" \"b\" \"c\"]] {:seed 42})\n; Execution error\n; Couldn't satisfy such-that predicate after 100 tries.\n\n;; GOOD: every value generated by the `:enum` is a string\n(mg/generate [:and [:enum \"a\" \"b\" \"c\"] :string] {:seed 42})\n; =\u003e \"a\"\n```\n\nYou might need to customize the generator for the first `:and` child to improve\nthe chances of it generating valid values.\n\nFor example, a schema for non-empty heterogeneous vectors can validate values\nby combining `:cat` and `vector?`, but since `:cat` generates sequences\nwe need to use `:gen/fmap` to make it generate vectors:\n\n```clojure\n;; generate a non-empty vector starting with a keyword\n(mg/generate [:and [:cat {:gen/fmap vec}\n                    :keyword [:* :any]]\n                   vector?]\n             {:size 1\n              :seed 2})\n;=\u003e [:.+ [1]]\n```\n\n## Inferring schemas\n\nInspired by [F# Type providers](https://docs.microsoft.com/en-us/dotnet/fsharp/tutorials/type-providers/):\n\n```clojure\n(require '[malli.provider :as mp])\n\n(def samples\n  [{:id \"Lillan\"\n    :tags #{:artesan :coffee :hotel}\n    :address {:street \"Ahlmanintie 29\"\n              :city \"Tampere\"\n              :zip 33100\n              :lonlat [61.4858322, 23.7854658]}}\n   {:id \"Huber\",\n    :description \"Beefy place\"\n    :tags #{:beef :wine :beer}\n    :address {:street \"Aleksis Kiven katu 13\"\n              :city \"Tampere\"\n              :zip 33200\n              :lonlat [61.4963599 23.7604916]}}])\n\n(mp/provide samples)\n;[:map\n; [:id :string]\n; [:tags [:set :keyword]]\n; [:address\n;  [:map\n;   [:street :string]\n;   [:city :string]\n;   [:zip :int]\n;   [:lonlat [:vector :double]]]]\n; [:description {:optional true} :string]]\n```\n\nAll samples are valid against the inferred schema:\n\n```clojure\n(every? (partial m/validate (mp/provide samples)) samples)\n; =\u003e true\n```\n\nFor better performance, use `mp/provider`:\n\n```clojure\n(require '[criterium.core :as p])\n\n;; 5ms\n(p/bench (mp/provide samples))\n\n;; 500µs (10x)\n(let [provider (mp/provider)]\n  (p/bench (provider samples)))\n```\n\n### :map-of inferring\n\nBy default, `:map-of` is not inferred:\n\n```clojure\n(mp/provide\n [{\"1\" [1]}\n  {\"2\" [1 2]}\n  {\"3\" [1 2 3]}])\n;[:map\n; [\"1\" {:optional true} [:vector :int]]\n; [\"2\" {:optional true} [:vector :int]]\n; [\"3\" {:optional true} [:vector :int]]]\n```\n\nWith `::mp/map-of-threshold` option:\n\n```clojure\n(mp/provide\n [{\"1\" [1]}\n  {\"2\" [1 2]}\n  {\"3\" [1 2 3]}]\n {::mp/map-of-threshold 3})\n; [:map-of :string [:vector :int]]\n```\n\nSample-data can be type-hinted with `::mp/hint`:\n\n```clojure\n(mp/provide\n  [^{::mp/hint :map-of}\n   {:a {:b 1, :c 2}\n    :b {:b 2, :c 1}\n    :c {:b 3}\n    :d nil}])\n;[:map-of\n; :keyword\n; [:maybe [:map\n;          [:b :int]\n;          [:c {:optional true} :int]]]]\n```\n\n### :tuple inferring\n\nBy default, tuples are not inferred:\n\n```clojure\n(mp/provide\n  [[1 \"kikka\" true]\n   [2 \"kukka\" true]\n   [3 \"kakka\" true]])\n; [:vector :some]\n```\n\nWith `::mp/tuple-threshold` option:\n\n```clojure\n(mp/provide\n  [[1 \"kikka\" true]\n   [2 \"kukka\" true]\n   [3 \"kakka\" false]]\n  {::mp/tuple-threshold 3})\n; [:tuple :int :string :boolean]\n```\n\nSample-data can be type-hinted with `::mp/hint`:\n\n```clojure\n(mp/provide\n  [^{::mp/hint :tuple}\n   [1 \"kikka\" true]\n   [\"2\" \"kukka\" true]])\n; [:tuple :some :string :boolean]\n```\n\n### value decoding in inferring\n\nBy default, no decoding is applied for (leaf) values:\n\n```clojure\n(mp/provide\n [{:id \"caa71a26-5fe1-11ec-bf63-0242ac130002\"}\n  {:id \"8aadbf5e-5fe3-11ec-bf63-0242ac130002\"}])\n; =\u003e [:map [:id :string]]\n```\n\nAdding custom decoding via `::mp/value-decoders` option:\n\n```clojure\n(mp/provide\n [{:id \"caa71a26-5fe1-11ec-bf63-0242ac130002\"\n   :time \"2021-01-01T00:00:00Z\"}\n  {:id \"8aadbf5e-5fe3-11ec-bf63-0242ac130002\"\n   :time \"2022-01-01T00:00:00Z\"}]\n {::mp/value-decoders {:string {:uuid mt/-string-\u003euuid\n                                'inst? mt/-string-\u003edate}}})\n; =\u003e [:map [:id :uuid] [:time inst?]]\n```\n\n## Destructuring\n\nSchemas can also be inferred from [Clojure Destructuring Syntax](https://clojure.org/guides/destructuring).\n\n```clojure\n(require '[malli.destructure :as md])\n\n(def infer (comp :schema md/parse))\n\n(infer '[a b \u0026 cs])\n; =\u003e [:cat :any :any [:* :any]]\n```\nMalli also supports adding type hints as an extension to the normal Clojure syntax (enabled by default), inspired by [Plumatic Schema](https://github.com/plumatic/schema#beyond-type-hints).\n\n```clojure\n(infer '[a :- :int, b :- :string \u0026 cs :- [:* :boolean]])\n; =\u003e [:cat :int :string [:* :boolean]]\n```\n\nPulling out function argument schemas from Vars:\n\n```clojure\n(defn kikka\n  ([a] [a])\n  ([a b \u0026 cs] [a b cs]))\n\n(md/infer #'kikka)\n;[:function\n; [:=\u003e [:cat :any] :any]\n; [:=\u003e [:cat :any :any [:* :any]] :any]]\n```\n\n`md/parse` uses the following options:\n\n| key                    | description |\n| -----------------------|-------------|\n| `::md/inline-schemas`  | support plumatic-style inline schemas (true)\n| `::md/sequential-maps` | support sequential maps in non-rest position (true)\n| `::md/references`      | qualified schema references used (true)\n| `::md/required-keys`   | destructured keys are required (false)\n| `::md/closed-maps`     | destructured maps are closed (false)\n\nA more complete example:\n\n```clojure\n(infer '[a [b c \u0026 rest :as bc]\n         \u0026 {:keys [d e]\n            :demo/keys [f]\n            g :demo/g\n            [h] :h\n            :or {d 0}\n            :as opts}])\n;[:cat\n; :any\n; [:maybe [:cat\n;          [:? :any]\n;          [:? :any]\n;          [:* :any]]]\n; [:altn\n;  [:map\n;   [:map\n;    [:d {:optional true} :any]\n;    [:e {:optional true} :any]\n;    [:demo/f {:optional true}]\n;    [:demo/g {:optional true}]\n;    [:h {:optional true} [:maybe [:cat\n;                                  [:? :any]\n;                                  [:* :any]]]]]]\n;  [:args\n;   [:*\n;    [:alt\n;     [:cat [:= :d] :any]\n;     [:cat [:= :e] :any]\n;     [:cat [:= :demo/f] :demo/f]\n;     [:cat [:= :demo/g] :demo/g]\n;     [:cat [:= :h] [:maybe [:cat\n;                            [:? :any]\n;                            [:* :any]]]]\n;     [:cat [:not [:enum :d :e :demo/f :demo/g :h]] :any]]]]]]\n```\n\n## Parsing values\n\nSchemas can be used to parse values using `m/parse` and `m/parser`:\n\n`m/parse` for one-time things:\n\n```clojure\n(m/parse\n  [:* [:catn\n       [:prop string?]\n       [:val [:altn\n              [:s string?]\n              [:b boolean?]]]]]\n  [\"-server\" \"foo\" \"-verbose\" true \"-user\" \"joe\"])\n;[#malli.core.Tags{:values {:prop \"-server\", :val #malli.core.Tag{:key :s, :value \"foo\"}}}\n; #malli.core.Tags{:values {:prop \"-verbose\", :val #malli.core.Tag{:key :b, :value true}}}\n; #malli.core.Tags{:values {:prop \"-user\", :val #malli.core.Tag{:key :s, :value \"joe\"}}}]\n\n```\n\n`m/parser` to create an optimized parser:\n\n```clojure\n(def Hiccup\n  [:schema {:registry {\"hiccup\" [:orn\n                                 [:node [:catn\n                                         [:name keyword?]\n                                         [:props [:? [:map-of keyword? any?]]]\n                                         [:children [:* [:schema [:ref \"hiccup\"]]]]]]\n                                 [:primitive [:orn\n                                              [:nil nil?]\n                                              [:boolean boolean?]\n                                              [:number number?]\n                                              [:text string?]]]]}}\n   \"hiccup\"])\n\n(def parse-hiccup (m/parser Hiccup))\n\n(parse-hiccup\n  [:div {:class [:foo :bar]}\n   [:p \"Hello, world of data\"]])\n\n;#malli.core.Tag\n;{:key :node,\n; :value\n; #malli.core.Tags\n; {:values {:name :div,\n;           :props {:class [:foo :bar]},\n;           :children [#malli.core.Tag\n;                      {:key :node,\n;                       :value\n;                       #malli.core.Tags\n;                       {:values {:name :p,\n;                                 :props nil,\n;                                 :children [#malli.core.Tag\n;                                            {:key :primitive,\n;                                             :value\n;                                             #malli.core.Tag\n;                                             {:key :text,\n;                                              :value \"Hello, world of data\"}}]}}}]}}}\n```\n\nParsing returns tagged values for `:orn`, `:catn`, `:altn` and `:multi`.\n\n```clojure\n(def Multi\n  [:multi {:dispatch :type}\n   [:user [:map [:size :int]]]\n   [::m/default :any]])\n\n(m/parse Multi {:type :user, :size 1})\n; =\u003e #malli.core.Tag{:key :user, :value {:type :user, :size 1}}\n\n(m/parse Multi {:type \"sized\", :size 1})\n; =\u003e #malli.core.Tag{:key :malli.core/default, :value {:type \"sized\", :size 1}}\n```\n\n## Unparsing values\n\nThe inverse of parsing, using `m/unparse` and `m/unparser`:\n\n```clojure\n(-\u003e\u003e [:div {:class [:foo :bar]}\n      [:p \"Hello, world of data\"]]\n     (m/parse Hiccup)\n     (m/unparse Hiccup))\n;[:div {:class [:foo :bar]}\n; [:p \"Hello, world of data\"]]\n```\n\n```clojure\n(m/unparse [:orn [:name :string] [:id :int]]\n           (m/tag :name \"x\"))\n; =\u003e \"x\"\n\n(m/unparse [:* [:catn [:name :string] [:id :int]]]\n           [(m/tags {:name \"x\" :id 1})\n            (m/tags {:name \"y\" :id 2})])\n; =\u003e [\"x\" 1 \"y\" 2]\n```\n\n## Serializable functions\n\nEnabling serializable function schemas requires [SCI](https://github.com/borkdude/sci) or [cherry](https://github.com/squint-cljs/cherry) (for client side) as external dependency. If\nit is not present, the malli function evaluator throws `:sci-not-available` exception.\n\nFor ClojureScript, you need to require `sci.core` or `malli.cherry` manually.\n\nFor GraalVM, you need to require `sci.core` manually, before requiring any malli namespaces.\n\n```clojure\n(def my-schema\n  [:and\n   [:map\n    [:x int?]\n    [:y int?]]\n   [:fn '(fn [{:keys [x y]}] (\u003e x y))]])\n\n(m/validate my-schema {:x 1, :y 0})\n; =\u003e true\n\n(m/validate my-schema {:x 1, :y 2})\n; =\u003e false\n```\n\n**NOTE**: [sci is not termination safe](https://github.com/borkdude/sci/issues/348) 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`.\n\n```clojure\n(m/validate [:fn 'int?] 1 {::m/disable-sci true})\n; Execution error\n; :malli.core/sci-not-available {:code int?}\n```\n\n## Schema AST\n\nImplemented with protocol `malli.core/AST`. Allows lossless round-robin with faster schema creation.\n\n**NOTE**: For now, the AST syntax in considered as internal, e.g. don't use it as a database persistency model.\n\n```clojure\n(def ?schema\n  [:map\n   [:x boolean?]\n   [:y {:optional true} int?]\n   [:z [:map\n        [:x boolean?]\n        [:y {:optional true} int?]]]])\n\n(m/form ?schema)\n;[:map\n; [:x boolean?]\n; [:y {:optional true} int?]\n; [:z [:map\n;      [:x boolean?]\n;      [:y {:optional true} int?]]]]\n\n(m/ast ?schema)\n;{:type :map,\n; :keys {:x {:order 0\n;            :value {:type boolean?}},\n;        :y {:order 1, :value {:type int?}\n;            :properties {:optional true}},\n;        :z {:order 2,\n;            :value {:type :map,\n;                    :keys {:x {:order 0\n;                               :value {:type boolean?}},\n;                           :y {:order 1\n;                               :value {:type int?}\n;                               :properties {:optional true}}}}}}}\n\n(-\u003e ?schema\n    (m/schema) ;; 3.4µs\n    (m/ast)\n    (m/from-ast) ;; 180ns (18x, lazy)\n    (m/form)\n    (= (m/form ?schema)))\n; =\u003e true\n```\n\n## Schema transformation\n\nSchemas can be transformed using post-walking, e.g. the [Visitor Pattern](https://en.wikipedia.org/wiki/visitor_pattern).\n\nThe identity walker:\n\n```clojure\n(m/walk\n  Address\n  (m/schema-walker identity))\n;[:map\n; [:id string?]\n; [:tags [:set keyword?]]\n; [:address\n;  [:map\n;   [:street string?]\n;   [:city string?]\n;   [:zip int?]\n;   [:lonlat [:tuple double? double?]]]]]\n```\n\nAdding `:title` property to schemas:\n\n```clojure\n(m/walk\n  Address\n  (m/schema-walker #(mu/update-properties % assoc :title (name (m/type %)))))\n;[:map {:title \"map\"}\n; [:id [string? {:title \"string?\"}]]\n; [:tags [:set {:title \"set\"} [keyword? {:title \"keyword?\"}]]]\n; [:address\n;  [:map {:title \"map\"}\n;   [:street [string? {:title \"string?\"}]]\n;   [:city [string? {:title \"string?\"}]]\n;   [:zip [int? {:title \"int?\"}]]\n;   [:lonlat [:tuple {:title \"tuple\"} [double? {:title \"double?\"}] [double? {:title \"double?\"}]]]]]]\n```\n\nTransforming schemas into maps:\n\n```clojure\n(m/walk\n  Address\n  (fn [schema _ children _]\n    (-\u003e (m/properties schema)\n        (assoc :malli/type (m/type schema))\n        (cond-\u003e (seq children) (assoc :malli/children children)))))\n;{:malli/type :map,\n; :malli/children [[:id nil {:malli/type string?}]\n;                  [:tags nil {:malli/type :set\n;                              :malli/children [{:malli/type keyword?}]}]\n;                  [:address nil {:malli/type :map,\n;                                 :malli/children [[:street nil {:malli/type string?}]\n;                                                  [:city nil {:malli/type string?}]\n;                                                  [:zip nil {:malli/type int?}]\n;                                                  [:lonlat nil {:malli/type :tuple\n;                                                                :malli/children [{:malli/type double?}\n;                                                                                 {:malli/type double?}]}]]}]]}\n```\n\n### JSON Schema\n\nTransforming Schemas into [JSON Schema](https://json-schema.org/):\n\n```clojure\n(require '[malli.json-schema :as json-schema])\n\n(json-schema/transform Address)\n;{:type \"object\",\n; :properties {:id {:type \"string\"},\n;              :tags {:type \"array\"\n;                     :items {:type \"string\"}\n;                     :uniqueItems true},\n;              :address {:type \"object\",\n;                        :properties {:street {:type \"string\"},\n;                                     :city {:type \"string\"},\n;                                     :zip {:type \"integer\", :format \"int64\"},\n;                                     :lonlat {:type \"array\",\n;                                              :items [{:type \"number\"} {:type \"number\"}],\n;                                              :additionalItems false}},\n;                        :required [:street :city :zip :lonlat]}},\n; :required [:id :tags :address]}\n```\n\nCustom transformation via `:json-schema` namespaced properties:\n\n```clojure\n(json-schema/transform\n  [:enum\n   {:title \"Fish\"\n    :description \"It's a fish\"\n    :json-schema/type \"string\"\n    :json-schema/default \"perch\"}\n   \"perch\" \"pike\"])\n;{:title \"Fish\"\n; :description \"It's a fish\"\n; :type \"string\"\n; :default \"perch\"\n; :enum [\"perch\" \"pike\"]}\n```\n\nFull override with `:json-schema` property:\n\n```clojure\n(json-schema/transform\n  [:map {:json-schema {:type \"file\"}}\n   [:file any?]])\n; {:type \"file\"}\n```\n\n### Swagger2\n\nTransforming Schemas into [Swagger2 Schema](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md):\n\n```clojure\n(require '[malli.swagger :as swagger])\n\n(swagger/transform Address)\n;{:type \"object\",\n; :properties {:id {:type \"string\"},\n;              :tags {:type \"array\"\n;                     :items {:type \"string\"}\n;                     :uniqueItems true},\n;              :address {:type \"object\",\n;                        :properties {:street {:type \"string\"},\n;                                     :city {:type \"string\"},\n;                                     :zip {:type \"integer\", :format \"int64\"},\n;                                     :lonlat {:type \"array\",\n;                                              :items {},\n;                                              :x-items [{:type \"number\", :format \"double\"}\n;                                                        {:type \"number\", :format \"double\"}]}},\n;                        :required [:street :city :zip :lonlat]}},\n; :required [:id :tags :address]}\n```\n\nCustom transformation via `:swagger` and `:json-schema` namespaced properties:\n\n```clojure\n(swagger/transform\n  [:enum\n   {:title \"Fish\"\n    :description \"It's a fish\"\n    :swagger/type \"string\"\n    :json-schema/default \"perch\"}\n   \"perch\" \"pike\"])\n;{:title \"Fish\"\n; :description \"It's a fish\"\n; :type \"string\"\n; :default \"perch\"\n; :enum [\"perch\" \"pike\"]}\n```\n\nFull override with `:swagger` property:\n\n```clojure\n(swagger/transform\n  [:map {:swagger {:type \"file\"}}\n   [:file any?]])\n; {:type \"file\"}\n```\n\n## Custom schema types\n\nSchema Types are described using `m/IntoSchema` protocol, which has a factory method\n`(-into-schema [this properties children options])` to create the actual Schema instances.\nSee `malli.core` for example implementations.\n\n### Simple schema\n\nFor simple cases, there is `m/-simple-schema`:\n\n```clojure\n(require '[clojure.test.check.generators :as gen])\n\n(def Over6\n  (m/-simple-schema\n    {:type :user/over6\n     :pred #(and (int? %) (\u003e % 6))\n     :type-properties {:error/message \"should be over 6\"\n                       :decode/string mt/-string-\u003elong\n                       :json-schema/type \"integer\"\n                       :json-schema/format \"int64\"\n                       :json-schema/minimum 6\n                       :gen/gen (gen/large-integer* {:min 7})}}))\n\n(m/into-schema? Over6)\n; =\u003e true\n```\n\n`m/IntoSchema` can be both used as Schema (creating a Schema instance with `nil` properties\nand children) and as Schema type to create new Schema instances without needing to\nregister the types:\n\n```clojure\n(m/schema? (m/schema Over6))\n; =\u003e true\n\n(m/schema? (m/schema [Over6 {:title \"over 6\"}]))\n; =\u003e true\n```\n\n`:pred` is used for validation:\n\n```clojure\n(m/validate Over6 2)\n; =\u003e false\n\n(m/validate Over6 7)\n; =\u003e true\n```\n\n`:type-properties` are shared for all schema instances and are used just like Schema\n(instance) properties by many Schema applications, including [error messages](#custom-error-messages),\n[value generation](#value-generation) and [json-schema](#json-schema) transformations.\n\n```clojure\n(json-schema/transform Over6)\n; =\u003e {:type \"integer\", :format \"int64\", :minimum 6}\n\n(json-schema/transform [Over6 {:json-schema/example 42}])\n; =\u003e {:type \"integer\", :format \"int64\", :minimum 6, :example 42}\n```\n\n### Content dependent simple schema\n\nYou can also build content-dependent schemas by using a callback function `:compile` of type `properties children options -\u003e opts`:\n\n```clojure\n(def Between\n  (m/-simple-schema\n   {:type `Between\n    :compile (fn [_properties [min max] _options]\n               (when-not (and (int? min) (int? max))\n                 (m/-fail! ::invalid-children {:min min, :max max}))\n               {:pred #(and (int? %) (\u003c= min % max))\n                :min 2 ;; at least 1 child\n                :max 2 ;; at most 1 child\n                :type-properties {:error/fn (fn [error _] (str \"should be between \" min \" and \" max \", was \" (:value error)))\n                                  :decode/string mt/-string-\u003elong\n                                  :json-schema {:type \"integer\"\n                                                :format \"int64\"\n                                                :minimum min\n                                                :maximum max}\n                                  :gen/gen (gen/large-integer* {:min (inc min), :max max})}})}))\n\n(m/form [Between 10 20])\n; =\u003e [user/Between 10 20]\n\n(-\u003e [Between 10 20]\n    (m/explain 8)\n    (me/humanize))\n; =\u003e [\"should be between 10 and 20, was 8\"]\n\n(mg/sample [Between -10 10])\n; =\u003e (-1 0 -2 -4 -4 0 -2 7 1 0)\n```\n\n## Schema registry\n\nSchemas 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.\n\nCustom `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.\n\n```clojure\n;; the default registry\n(m/validate [:maybe string?] \"kikka\")\n; =\u003e true\n\n;; registry as explicit options\n(m/validate [:maybe string?] \"kikka\" {:registry m/default-registry})\n; =\u003e true\n```\n\nThe default immutable registry is merged from multiple parts, enabling easy re-composition of custom schema sets. See [built-in schemas](#built-in-schemas) for list of all Schemas.\n\n### Custom registry\n\nHere's an example to create a custom registry without the default core predicates and with `:neg-int` and `:pos-int` Schemas:\n\n```clojure\n(def registry\n  (merge\n    (m/class-schemas)\n    (m/comparator-schemas)\n    (m/base-schemas)\n    {:neg-int (m/-simple-schema {:type :neg-int, :pred neg-int?})\n     :pos-int (m/-simple-schema {:type :pos-int, :pred pos-int?})}))\n\n(m/validate [:or :pos-int :neg-int] 'kikka {:registry registry})\n; =\u003e false\n\n(m/validate [:or :pos-int :neg-int] 123 {:registry registry})\n; =\u003e true\n```\n\nWe did not register normal predicate schemas:\n\n```clojure\n(m/validate pos-int? 123 {:registry registry})\n; Syntax error (ExceptionInfo) compiling\n; :malli.core/invalid-schema {:schema pos-int?}\n```\n\n### Local registry\n\nAny schema can define a local registry using `:registry` schema property:\n\n```clojure\n(def Adult\n  [:map {:registry {::age [:and int? [:\u003e 18]]}}\n   [:age ::age]])\n\n(mg/generate Adult {:size 10, :seed 1})\n; =\u003e {:age 92}\n```\n\nLocal registries can be persisted:\n\n```clojure\n(-\u003e Adult\n    (malli.edn/write-string)\n    (malli.edn/read-string)\n    (m/validate {:age 46}))\n; =\u003e true\n```\n\nSee also [Recursive Schemas](#recursive-schemas).\n\n### Changing the default registry\n\nPassing 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:\n\n```clojure\n(require '[malli.registry :as mr])\n\n;; the default registry\n(-\u003e m/default-registry (mr/schemas) (count))\n;=\u003e 140\n\n;; global side-effects! free since 0.7.0!\n(mr/set-default-registry!\n  {:string (m/-string-schema)\n   :maybe (m/-maybe-schema)\n   :map (m/-map-schema)})\n\n(-\u003e m/default-registry (mr/schemas) (count))\n; =\u003e 3\n\n(m/validate\n  [:map [:maybe [:maybe :string]]]\n  {:maybe \"sheep\"})\n; =\u003e true\n\n(m/validate :int 42)\n; =throws=\u003e :malli.core/invalid-schema {:schema :int}\n```\n\n**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:\n* cljs: `:closure-defines {malli.registry/mode \"strict\"}`\n* clj: `:jvm-opts [\"-Dmalli.registry/mode=strict\"]`\n\n### DCE and schemas\n\nThe 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.\n\nMalli allows the default registry to initialized with empty schemas, using the following compiler/jvm bootstrap:\n   * cljs: `:closure-defines {malli.registry/type \"custom\"}`\n   * clj: `:jvm-opts [\"-Dmalli.registry/type=custom\"]`\n\n```clojure\n;; with the flag set on\n(-\u003e m/default-registry (mr/schemas) (count))\n; =\u003e 0\n```\n\nWith this, you can register just what you need and rest are DCE'd. The previous example results in just a 3KB gzip bundle.\n\n## Registry implementations\n\nMalli supports multiple type of registries.\n\n### Immutable registry\n\nJust a `Map`.\n\n```clojure\n(require '[malli.registry :as mr])\n\n(mr/set-default-registry!\n  {:string (m/-string-schema)\n   :maybe (m/-maybe-schema)\n   :map (m/-map-schema)})\n\n(m/validate\n  [:map [:maybe [:maybe :string]]]\n  {:maybe \"sheep\"})\n; =\u003e true\n```\n### Var registry\n\nVar 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.\n\n```clojure\n(def UserId :string)\n\n(def User\n  [:map\n   [:id #'UserId]\n   [:friends {:optional true} [:set [:ref #'User]]]])\n\n(mg/sample User {:seed 0})\n;({:id \"\"}\n; {:id \"6\", :friends #{{:id \"\"}}}\n; {:id \"\"}\n; {:id \"4\", :friends #{}}\n; {:id \"24b7\"}\n; {:id \"Uo\"}\n; {:id \"8\"}\n; {:id \"z5b\"}\n; {:id \"R9f\"}\n; {:id \"lUm6Wj9gR\"})\n```\n\n### Mutable registry\n\n[clojure.spec](https://clojure.org/guides/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:\n\nUsing a custom registry atom:\n\n```clojure\n(def registry*\n  (atom {:string (m/-string-schema)\n         :maybe (m/-maybe-schema)\n         :map (m/-map-schema)}))\n\n(defn register! [type ?schema]\n  (swap! registry* assoc type ?schema))\n\n(mr/set-default-registry!\n  (mr/mutable-registry registry*))\n\n(register! :non-empty-string [:string {:min 1}])\n\n(m/validate :non-empty-string \"malli\")\n; =\u003e true\n```\n\nThe mutable registry can also be passed in as an explicit option:\n\n```clojure\n(def registry (mr/mutable-registry registry*))\n\n(m/validate :non-empty-string \"malli\" {:registry registry})\n; =\u003e true\n```\n\n### Dynamic registry\n\nIf you know what you are doing, you can also use [dynamic scope](https://stuartsierra.com/2013/03/29/perils-of-dynamic-scope) to pass in default schema registry:\n\n```clojure\n(mr/set-default-registry!\n  (mr/dynamic-registry))\n\n(binding [mr/*registry* {:string (m/-string-schema)\n                         :maybe (m/-maybe-schema)\n                         :map (m/-map-schema)\n                         :non-empty-string [:string {:min 1}]}]\n  (m/validate :non-empty-string \"malli\"))\n; =\u003e true\n```\n\n### Lazy registries\n\nYou can provide schemas at runtime using `mr/lazy-registry` - it takes a local registry and a provider function of `type registry -\u003e schema` as arguments:\n\n```clojure\n(def registry\n  (mr/lazy-registry\n    (m/default-schemas)\n    (fn [type registry]\n      ;; simulates pulling CloudFormation Schemas when needed\n      (let [lookup {\"AWS::ApiGateway::UsagePlan\" [:map {:closed true}\n                                                  [:Type [:= \"AWS::ApiGateway::UsagePlan\"]]\n                                                  [:Description {:optional true} string?]\n                                                  [:UsagePlanName {:optional true} string?]]\n                    \"AWS::AppSync::ApiKey\" [:map {:closed true}\n                                            [:Type [:= \"AWS::AppSync::ApiKey\"]]\n                                            [:ApiId string?]\n                                            [:Description {:optional true} string?]]}]\n        (println \"... loaded\" type)\n        (some-\u003e type lookup (m/schema {:registry registry}))))))\n\n;; lazy multi, doesn't realize the schemas\n(def CloudFormation\n  (m/schema\n    [:multi {:dispatch :Type, :lazy-refs true}\n     \"AWS::ApiGateway::UsagePlan\"\n     \"AWS::AppSync::ApiKey\"]\n    {:registry registry}))\n\n(m/validate\n  CloudFormation\n  {:Type \"AWS::ApiGateway::UsagePlan\"\n   :Description \"laiskanlinna\"})\n; ... loaded AWS::ApiGateway::UsagePlan\n; =\u003e true\n\n(m/validate\n  CloudFormation\n  {:Type \"AWS::ApiGateway::UsagePlan\"\n   :Description \"laiskanlinna\"})\n; =\u003e true\n```\n\n### Composite registry\n\nRegistries can be composed, a full example:\n\n```clojure\n(require '[malli.core :as m])\n(require '[malli.registry :as mr])\n\n(def registry (atom {}))\n\n(defn register! [type schema]\n  (swap! registry assoc type schema))\n\n(mr/set-default-registry!\n  ;; linear search\n  (mr/composite-registry\n    ;; immutable registry\n    {:map (m/-map-schema)}\n    ;; mutable (spec-like) registry\n    (mr/mutable-registry registry)\n    ;; on the perils of dynamic scope\n    (mr/dynamic-registry)))\n\n;; mutate like a boss\n(register! :maybe (m/-maybe-schema))\n\n;; ☆.。.:*・°☆.。.:*・°☆.。.:*・°☆.。.:*・°☆\n(binding [mr/*registry* {:string (m/-string-schema)}]\n  (m/validate\n    [:map [:maybe [:maybe :string]]]\n    {:maybe \"sheep\"}))\n; =\u003e true\n```\n\n## Function schemas\n\nSee [Working with functions](docs/function-schemas.md).\n\n### Instrumentation\n\nSee [Instrumentation](docs/function-schemas.md#instrumentation).\n\n## Clj-kondo\n\n[Clj-kondo](https://github.com/borkdude/clj-kondo) is a linter for Clojure code that sparks joy.\n\nGiven functions and function Schemas:\n\n```clojure\n(defn square [x] (* x x))\n(m/=\u003e square [:=\u003e [:cat int?] nat-int?])\n\n(defn plus\n  ([x] x)\n  ([x y] (+ x y)))\n\n(m/=\u003e plus [:function\n            [:=\u003e [:cat int?] int?]\n            [:=\u003e [:cat int? int?] int?]])\n```\n\nGenerating `clj-kondo` configuration from current namespace:\n\n```clojure\n(require '[malli.clj-kondo :as mc])\n\n(-\u003e (mc/collect *ns*) (mc/linter-config))\n;{:lint-as #:malli.schema{defn schema.core/defn},\n; :linters\n; {:type-mismatch\n;  {:namespaces\n;   {user {square {:arities {1 {:args [:int]\n;                               :ret :pos-int}}}\n;          plus {:arities {1 {:args [:int]\n;                             :ret :int},\n;                          2 {:args [:int :int]\n;                             :ret :int}}}}}}}}\n```\n\nEmitting confing into `./.clj-kondo/configs/malli/config.edn`:\n\n```clojure\n(mc/emit!)\n```\n\nIn action:\n\n![malli](docs/img/clj-kondo.png)\n\n## Static type checking via Typed Clojure\n\n[Typed Clojure](https://github.com/typedclojure/typedclojure) is an optional type system for Clojure.\n\n[typed.malli](https://github.com/typedclojure/typedclojure/tree/main/typed/malli) can consume a subset of malli\nschema syntax to statically type check and infer Clojure code.\n\nSee this in action in the [malli-type-providers](https://github.com/typedclojure/typedclojure/tree/main/example-projects/malli-type-providers)\nexample project.\n\n```clojure\n(ns typed-example.malli-type-providers\n  (:require [typed.clojure :as t]\n            [malli.core :as m]))\n\n;; just use malli instrumentation normally\n(m/=\u003e foo [:=\u003e [:cat :int] :int])\n;; Typed Clojure will statically check `foo` against its schema (after converting it to a type)\n(defn foo [t] (inc t))\n;; Typed Clojure will automatically infer `foo`s type from its schema\n(foo 1)\n\n(comment (t/check-ns-clj)) ;; check this ns\n```\n\n## Visualizing schemas\n\n### DOT\n\nTransforming Schemas into [DOT Language](https://en.wikipedia.org/wiki/DOT_(graph_description_language) ):\n\n```clojure\n(require '[malli.dot :as md])\n\n(def Address\n  [:schema\n   {:registry {\"Country\" [:map\n                          [:name [:enum :FI :PO]]\n                          [:neighbors [:vector [:ref \"Country\"]]]]\n               \"Burger\" [:map\n                         [:name string?]\n                         [:description {:optional true} string?]\n                         [:origin [:maybe \"Country\"]]\n                         [:price pos-int?]]\n               \"OrderLine\" [:map\n                            [:burger \"Burger\"]\n                            [:amount int?]]\n               \"Order\" [:map\n                        [:lines [:vector \"OrderLine\"]]\n                        [:delivery [:map\n                                    [:delivered boolean?]\n                                    [:address [:map\n                                               [:street string?]\n                                               [:zip int?]\n                                               [:country \"Country\"]]]]]]}}\n   \"Order\"])\n\n(md/transform Address)\n; \"digraph { ... }\"\n```\n\nVisualized with [Graphviz](https://graphviz.org/):\n\n![Graphviz image output](/docs/img/dot.png)\n\n### PlantUML\n\nTransforming Schemas into [PlantUML](https://plantuml.com/):\n\n```clojure\n(require '[malli.plantuml :as plantuml])\n\n(plantuml/transform Address)\n; \"@startuml ... @enduml\"\n```\n\nVisualized with [PlantText](https://www.planttext.com/):\n\n![PlanText image output](/docs/img/plantuml.png)\n\n## Lite\n\nSimple syntax sugar, like [data-specs](https://cljdoc.org/d/metosin/spec-tools/CURRENT/doc/data-specs), but for malli.\n\nAs the namespace suggests, it's experimental, built for [reitit](https://github.com/metosin/reitit).\n\n```clojure\n(require '[malli.experimental.lite :as l])\n\n(l/schema\n {:map1 {:x int?\n         :y [:maybe string?]\n         :z (l/maybe keyword?)}\n  :map2 {:min-max [:int {:min 0 :max 10}]\n         :tuples (l/vector (l/tuple int? string?))\n         :optional (l/optional (l/maybe :boolean))\n         :set-of-maps (l/set {:e int?\n                              :f string?})\n         :map-of-int (l/map-of int? {:s string?})}})\n;[:map\n; [:map1\n;  [:map\n;   [:x int?]\n;   [:y [:maybe string?]]\n;   [:z [:maybe keyword?]]]]\n; [:map2\n;  [:map\n;   [:min-max [:int {:min 0, :max 10}]]\n;   [:tuples [:vector [:tuple int? string?]]]\n;   [:optional {:optional true} [:maybe :boolean]]\n;   [:set-of-maps [:set [:map [:e int?] [:f string?]]]]\n;   [:map-of-int [:map-of int? [:map [:s string?]]]]]]]\n```\n\nOptions can be used by binding a dynamic `l/*options*` Var:\n\n```clojure\n(binding [l/*options* {:registry (merge\n                                  (m/default-schemas)\n                                  {:user/id :int})}]\n  (l/schema {:id (l/maybe :user/id)\n             :child {:id :user/id}}))\n;[:map\n; [:id [:maybe :user/id]]\n; [:child [:map [:id :user/id]]]]\n```\n\n## Performance\n\nMalli tries to be really, really fast.\n\n### Validation performance\n\nUsually as fast (or faster) as idiomatic Clojure.\n\n```clojure\n(require '[criterium.core :as cc])\n\n(def valid {:x true, :y 1, :z \"zorro\"})\n\n;; idomatic clojure (54ns)\n(let [valid? (fn [{:keys [x y z]}]\n               (and (boolean? x)\n                    (if y (int? y) true)\n                    (string? z)))]\n  (assert (valid? valid))\n  (cc/quick-bench (valid? valid)))\n\n(require '[malli.core :as m])\n\n;; malli (39ns)\n(let [valid? (m/validator\n               [:map\n                [:x :boolean]\n                [:y {:optional true} :int]\n                [:z :string]])]\n  (assert (valid? valid))\n  (cc/quick-bench (valid? valid)))\n```\n\nSame with Clojure Spec and Plumatic Schema:\n\n```clojure\n(require '[clojure.spec.alpha :as spec])\n(require '[schema.core :as schema])\n\n(spec/def ::x boolean?)\n(spec/def ::y int?)\n(spec/def ::z string?)\n\n;; clojure.spec (450ns)\n(let [spec (spec/keys :req-un [::x ::z] :opt-un [::y])]\n  (assert (spec/valid? spec valid))\n  (cc/quick-bench (spec/valid? spec valid)))\n\n;; plumatic schema (660ns)\n(let [valid? (schema/checker\n               {:x schema/Bool\n                (schema/optional-key :y) schema/Int\n                :z schema/Str})]\n  (assert (not (valid? valid)))\n  (cc/quick-bench (valid? valid)))\n```\n\n### Transformation performance\n\nUsually faster than idiomatic Clojure.\n\n```clojure\n(def data {:x \"true\", :y \"1\", :z \"kikka\"})\n(def expected {:x true, :y 1, :z \"kikka\"})\n\n;; idiomatic clojure (290ns)\n(let [transform (fn [{:keys [x y] :as m}]\n                  (cond-\u003e m\n                    (string? x) (update :x #(Boolean/parseBoolean %))\n                    (string? y) (update :y #(Long/parseLong %))))]\n  (assert (= expected (transform data)))\n  (cc/quick-bench (transform data)))\n\n;; malli (72ns)\n(let [schema [:map\n              [:x :boolean]\n              [:y {:optional true} int?]\n              [:z string?]]\n      transform (m/decoder schema (mt/string-transformer))]\n  (assert (= expected (transform data)))\n  (cc/quick-bench (transform data)))\n```\n\nSame with Clojure Spec and Plumatic Schema:\n\n```clojure\n(require '[spec-tools.core :as st])\n(require '[schema.coerce :as sc])\n\n(spec/def ::x boolean?)\n(spec/def ::y int?)\n(spec/def ::z string?)\n\n;; clojure.spec (19000ns)\n(let [spec (spec/keys :req-un [::x ::z] :opt-un [::y])\n      transform #(st/coerce spec % st/string-transformer)]\n  (assert (= expected (transform data)))\n  (cc/quick-bench (transform data)))\n\n;; plumatic schema (2200ns)\n(let [schema {:x schema/Bool\n              (schema/optional-key :y) schema/Int\n              :z schema/Str}\n      transform (sc/coercer schema sc/string-coercion-matcher)]\n  (assert (= expected (transform data)))\n  (cc/quick-bench (transform data)))\n```\n\nThe 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.\n\n```clojure\n(def json-\u003euser\n  (m/decoder\n    [:map\n     [:id :int]\n     [:name :string]\n     [:address [:map\n                [:street :string]\n                [:rural :boolean]\n                [:country [:enum \"finland\" \"poland\"]]]]]\n    (mt/json-transformer)))\n\n(= identity json-\u003euser)\n; =\u003e true\n\n;; 5ns\n(cc/quick-bench\n  (json-\u003euser\n    {:id 1\n     :name \"tiina\"\n     :address {:street \"kotikatu\"\n               :rural true\n               :country \"poland\"}}))\n```\n\n### Parsing performance\n\n```clojure\n;; 37µs\n(let [spec (s/* (s/cat :prop string?,\n                       :val (s/alt :s string?\n                                   :b boolean?)))\n      parse (partial s/conform spec)]\n  (cc/quick-bench\n    (parse [\"-server\" \"foo\" \"-verbose\" \"-verbose\" \"-user\" \"joe\"])))\n\n;; 2.4µs\n(let [schema [:* [:catn\n                  [:prop string?]\n                  [:val [:altn\n                         [:s string?]\n                         [:b boolean?]]]]]\n      parse (m/parser schema)]\n  (cc/quick-bench\n    (parse [\"-server\" \"foo\" \"-verbose\" \"-verbose\" \"-user\" \"joe\"])))\n```\n\n## Built-in schemas\n\n### `malli.core/predicate-schemas`\n\nContains 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?`.\n\n*NOTE*: Predicate Schemas do not cover any schema properties, e.g. `string?` can't be modified with properties like `:min` and `:max`. If you want to use the schema properties, use real schema types instead, e.g. `:string` over `string?`.\n\n### `malli.core/class-schemas`\n\nClass-based schemas, contains `java.util.regex.Pattern` \u0026 `js/RegExp`.\n\n### `malli.core/comparator-schemas`\n\nComparator functions as keywords: `:\u003e`, `:\u003e=`, `:\u003c`, `:\u003c=`, `:=` and `:not=`.\n\n### `malli.core/type-schemas`\n\nType-like schemas: `:any`, `:some`, `:nil`, `:string`, `:int`, `:double`, `:boolean`, `:keyword`, `:qualified-keyword`, `:symbol`, `:qualified-symbol`, and `:uuid`.\n\n### `malli.core/sequence-schemas`\n\nSequence/regex-schemas: `:+`, `:*`, `:?`, `:repeat`, `:cat`, `:alt`, `:catn`, `:altn`.\n\n### `malli.core/base-schemas`\n\nContains `:and`, `:or`, `:orn`, `:not`, `:map`, `:map-of`, `:vector`, `:sequential`, `:set`, `:enum`, `:maybe`, `:tuple`, `:multi`, `:re`, `:fn`, `:ref`, `:=\u003e`, `:-\u003e`, `:function` and `:schema`.\n\n### `malli.util/schemas`\n\n`:merge`, `:union` and `:select-keys`.\n\n### `malli.experimental.time`\n\nThe `time` namespace adds support for time formats as defined by [ISO 8601 - Date and time — Representations for information interchange](https://en.wikipedia.org/wiki/ISO_8601).\n\nCurrently supported platform and providing implementations:\n\n- JVM: via the java.time package.\n- JS:  via the js-joda [package](https://github.com/js-joda/js-joda)\n\nThe following schemas and their respective types are provided:\n\n| Schema                   |","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmetosin%2Fmalli","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmetosin%2Fmalli","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmetosin%2Fmalli/lists"}