# Tips ## Accessing both schema and value in transformation ```clojure (require '[malli.core :as m]) (require '[malli.transform :as mt]) (def Address [:map [:id :string] [:tags [:set :keyword]] [:address [:map [:street :string] [:city :string]]]]) (def lillan {:id "Lillan" :tags #{:artesan :coffee :hotel} :address {:street "Ahlmanintie 29" :city "Tampere"}}) (m/decode Address lillan (mt/transformer {:default-decoder {:compile (fn [schema _] (fn [value] (prn [value (m/form schema)]) value))}})) ;[{:id "Lillan", :tags #{:coffee :artesan :hotel}, :address {:street "Ahlmanintie 29", :city "Tampere"}} [:map [:id :string] [:tags [:set :keyword]] [:address [:map [:street :string] [:city :string]]]]] ;["Lillan" [:malli.core/val :string]] ;["Lillan" :string] ;[#{:coffee :artesan :hotel} [:malli.core/val [:set :keyword]]] ;[#{:coffee :artesan :hotel} [:set :keyword]] ;[:coffee :keyword] ;[:artesan :keyword] ;[:hotel :keyword] ;[{:street "Ahlmanintie 29", :city "Tampere"} [:malli.core/val [:map [:street :string] [:city :string]]]] ;[{:street "Ahlmanintie 29", :city "Tampere"} [:map [:street :string] [:city :string]]] ;["Ahlmanintie 29" [:malli.core/val :string]] ;["Ahlmanintie 29" :string] ;["Tampere" [:malli.core/val :string]] ;["Tampere" :string] ;; => {:id "Lillan", :tags #{:coffee :artesan :hotel}, :address {:street "Ahlmanintie 29", :city "Tampere"}} ``` ## Removing Schemas based on a property Schemas can be walked over recursively using `m/walk`: ```clojure (require '[malli.core :as m]) (def Schema [:map [:user :map] [:profile :map] [:tags [:vector [:int {:deleteMe true}]]] [:nested [:map [:x [:tuple {:deleteMe true} :string :string]]]] [:token [:string {:deleteMe true}]]]) (m/walk Schema (fn [schema _ children options] ;; return nil if Schema has the property (when-not (:deleteMe (m/properties schema)) ;; there are two syntaxes: normal and the entry, handle separately (let [children (if (m/entries schema) (filterv last children) children)] ;; create a new Schema with the updated children, or return nil (try (m/into-schema (m/type schema) (m/properties schema) children options) (catch #?(:clj Exception, :cljs js/Error) _)))))) ;[:map ; [:user :map] ; [:profile :map] ; [:nested :map]] ``` In the example, `:tags` key was removed as it's contents would have been an empty `:vector`, which is not legal Schema syntax. Empty `:map` is ok. ## Trimming strings Example how to trim all `:string` values using a custom transformer: ```clojure (require '[malli.transform :as mt]) (require '[malli.core :as m]) (require '[clojure.string :as str]) ;; a decoding transformer, only mounting to :string schemas with truthy :string/trim property (defn string-trimmer [] (mt/transformer {:decoders {:string {:compile (fn [schema _] (let [{:string/keys [trim]} (m/properties schema)] (when trim #(cond-> % (string? %) str/trim))))}}})) ;; trim me please (m/decode [:string {:string/trim true, :min 1}] " kikka " string-trimmer) ;; => "kikka" ;; no trimming (m/decode [:string {:min 1}] " " string-trimmer) ;; => " " ;; without :string/trim, decoding is a no-op (m/decoder :string string-trimmer) ; => #object[clojure.core$identity] ``` ## Decoding collections Transforming a comma-separated string into a vector of ints: ```clojure (require '[malli.core :as m]) (require '[malli.transform :as mt]) (require '[clojure.string :as str]) (m/decode [:vector {:decode/string #(str/split % #",")} int?] "1,2,3,4" (mt/string-transformer)) ;; => [1 2 3 4] ``` Using a custom transformer: ```clojure (defn query-decoder [schema] (m/decoder schema (mt/transformer (mt/transformer {:name "vectorize strings" :decoders {:vector {:compile (fn [schema _] (let [separator (-> schema m/properties :query/separator (or ","))] (fn [x] (cond (not (string? x)) x (str/includes? x separator) (into [] (.split ^String x separator)) :else [x]))))}}}) (mt/string-transformer)))) (def decode (query-decoder [:map [:a [:vector {:query/separator ";"} :int]] [:b [:vector :int]]])) (decode {:a "1", :b "1"}) ;; => {:a [1], :b [1]} (decode {:a "1;2", :b "1,2"}) ;; => {:a [1 2], :b [1 2]} ``` ## Normalizing properties Returning a Schema form with `nil` in place of empty properties: ```clojure (require '[malli.core :as m]) (defn normalize-properties [?schema] (m/walk ?schema (fn [schema _ children _] (if (vector? (m/form schema)) (into [(m/type schema) (m/properties schema)] children) (m/form schema))))) (normalize-properties [:map [:x :int] [:y [:tuple :int :int]] [:z [:set [:map [:x [:enum 1 2 3]]]]]]) ;; => [:map nil ;; [:x nil :int] ;; [:y nil [:tuple nil :int :int]] ;; [:z nil [:set nil ;; [:map nil ;; [:x nil [:enum nil 1 2 3]]]]]] ``` ## Default value from a function The `mt/default-value-transformer` can fill default values if the `:default` property is given. It is possible though to calculate a default value with a given function providing custom transformer derived from `mt/default-value-transformer`: ```clojure (defn default-fn-value-transformer ([] (default-fn-value-transformer nil)) ([{:keys [key] :or {key :default-fn}}] (let [add-defaults {:compile (fn [schema _] (let [->k-default (fn [[k {default key :keys [optional]} v]] (when-not optional (when-some [default (or default (some-> v m/properties key))] [k default]))) defaults (into {} (keep ->k-default) (m/children schema)) exercise (fn [x defaults] (reduce-kv (fn [acc k v] ; the key difference compare to default-value-transformer ; we evaluate v instead of just passing it (if-not (contains? x k) (-> (assoc acc k ((m/eval v) x)) (try (catch Exception _ acc))) acc)) x defaults))] (when (seq defaults) (fn [x] (if (map? x) (exercise x defaults) x)))))}] (mt/transformer {:decoders {:map add-defaults} :encoders {:map add-defaults}})))) ``` Example 1: if `:secondary` is missing, same its value to value of `:primary` ```clojure (m/decode [:map [:primary :string] [:secondary {:default-fn '#(:primary %)} :string]] {:primary "blue"} (default-fn-value-transformer)) ``` Example 2: if `:cost` is missing, try to calculate it from `:price` and `:qty`: ```clojure (def Purchase [:map [:qty {:default 1} number?] [:price {:optional true} number?] [:cost {:default-fn '(fn [m] (* (:qty m) (:price m)))} number?]]) (def decode-autonomous-vals (m/decoder Purchase (mt/transformer (mt/string-transformer) (mt/default-value-transformer)))) (def decode-interconnected-vals (m/decoder Purchase (default-fn-value-transformer))) (-> {:qty "100" :price "1.2"} decode-autonomous-vals decode-interconnected-vals) ;; => {:price 1.2, :qty 100.0, :cost 120.0} (-> {:price "1.2"} decode-autonomous-vals decode-interconnected-vals) ;; => {:qty 1, :price 1.2, :cost 1.2} (-> {:prie "1.2"} decode-autonomous-vals decode-interconnected-vals) ;; => {:prie "1.2", :qty 1} ``` ## Walking Schema and Entry Properties 1. walk entries on the way in 2. unwalk entries on the way out ```clojure (defn walk-properties [schema f] (m/walk schema (fn [s _ c _] (m/into-schema (m/-parent s) (f (m/-properties s)) (cond->> c (m/entries s) (map (fn [[k p s]] [k (f p) (first (m/children s))]))) (m/options s))) {::m/walk-entry-vals true})) ``` Stripping all swagger-keys: ```clojure (defn remove-swagger-keys [p] (not-empty (reduce-kv (fn [acc k _] (cond-> acc (some #{:swagger} [k (-> k namespace keyword)]) (dissoc k))) p p))) (walk-properties [:map {:title "Organisation name"} [:ref {:swagger/description "Reference to the organisation" :swagger/example "Acme floor polish, Houston TX"} :string] [:kikka [:string {:swagger {:title "kukka"}}]]] remove-swagger-keys) ;[:map {:title "Organisation name"} ; [:ref :string] ; [:kikka :string]] ``` ## Allowing invalid values on optional keys e.g. don't fail if the optional keys hava invalid values. 1. create a helper function that transforms the schema swapping the actual schema with `:any` 2. done. ```clojure (require '[malli.util :as mu]) (defn allow-invalid-optional-values [schema] (m/walk schema (m/schema-walker (fn [s] (cond-> s (m/entries s) (mu/transform-entries (partial map (fn [[k {:keys [optional] :as p} s]] [k p (if optional :any s)])))))))) (allow-invalid-optional-values [:map [:a :string] [:b {:optional true} :int] [:c [:maybe [:map [:d :string] [:e {:optional true} :int]]]]]) ;[:map ; [:a :string] ; [:b {:optional true} :any] ; [:c [:maybe [:map ; [:d :string] ; [:e {:optional true} :any]]]]] (m/validate [:map [:a :string] [:b {:optional true} :int]] {:a "Hey" :b "Nope"}) ;; => false (m/validate (allow-invalid-optional-values [:map [:a :string] [:b {:optional true} :int]]) {:a "Hey" :b "Nope"}) ;; => true ``` ## Collecting inlined reference definitions from schemas By default, one can inline schema reference definitions with `:map`, like: ```clojure (def User [:map [::id :int] [:name :string] [::country {:optional true} :string]]) ``` It would be nice to be able to simplify the schemas into: ```clojure [:map ::id [:name :string] [::country {:optional true}]] ``` Use cases: * Simplify large schemas * Finding differences in semantics * Refactoring multiple schemas to use a shared registry Naive implementation (doesn't look up the local registries): ```clojure (require '[malli.registry :as mr]) (defn collect-references [schema] (let [acc* (atom {}) ->registry (fn [registry] (->> (for [[k d] registry] (if (seq (rest d)) (m/-fail! ::ambiguous-references {:data d}) [k (first (keys d))])) (into {}))) schema (m/walk schema (fn [schema path children _] (let [children (if (= :map (m/type schema)) ;; just maps (->> children (mapv (fn [[k p s]] ;; we found inlined references (if (and (m/-reference? k) (not (m/-reference? s))) (do (swap! acc* update-in [k (m/form s)] (fnil conj #{}) (conj path k)) (if (seq p) [k p] k)) [k p s])))) children) ;; accumulated registry, fail on ambiguous refs registry (->registry @acc*)] ;; return simplified schema (m/into-schema (m/-parent schema) (m/-properties schema) children {:registry (mr/composite-registry (m/-registry (m/options schema)) registry)}))))] {:registry (->registry @acc*) :schema schema})) ``` In action: ```clojure (collect-references User) ;{:registry {:user/id :int, ; :user/country :string} ; :schema [:map ; :user/id ; [:name :string] ; [:user/country {:optional true}]]} ``` ```clojure (collect-references [:map [:user/id :int] [:child [:map [:user/id :string]]]]) ; =throws=> :user/ambiguous-references {:data {:string #{[:child :user/id]}, :int #{[:user/id]}}} ``` ## Getting error-values into humanized result ```clojure (require '[malli.error :as me]) (-> [:map [:x :int] [:y [:set :keyword]] [:z [:map [:a [:tuple :int :int]]]]] (m/explain {:x "1" :y #{:a "b" :c} :z {:a [1 "2"]}}) (me/humanize {:wrap #(select-keys % [:value :message])})) ;; => {:x [{:value "1" ; :message "should be an integer"}], ; :y #{[{:value "b" ; :message "should be a keyword"}]}, ; :z {:a [nil [{:value "2" ; :message "should be an integer"}]]}} ``` ## Dependent String Schemas A schema for a string made of two components `a` and `b` separated by a `/` where the schema of `b` depends on the value of `a`. The valid values of a are known in advance. For instance: * When `a` is "ip" , `b` should be a valid ip * When `a` is "domain", `b` should be a valid domain Here are a few examples of valid and invalid data: * `"ip/127.0.0.1"` is valid * `"ip/111"` is not valid * `"domain/cnn.com"` is valid * `"domain/aa"` is not valid * `"kika/aaa"` is not valid ```clojure (def domain #"[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+") (def ipv4 #"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$") ;; a multi schema describing the values as a tuple ;; includes transformation guide to and from a string domain (def schema [:multi {:dispatch first :decode/string #(str/split % #"/") :encode/string #(str/join "/" %)} ["domain" [:tuple [:= "domain"] domain]] ["ip" [:tuple [:= "ip"] ipv4]]]) ;; define workers (def validate (m/validator schema)) (def decode (m/decoder schema mt/string-transformer)) (def encode (m/encoder schema mt/string-transformer)) (decode "ip/127.0.0.1") ;; => ["ip" "127.0.0.1"] (-> "ip/127.0.0.1" (decode) (encode)) ;; => "ip/127.0.0.1" (map (comp validate decode) ["ip/127.0.0.1" "ip/111" "domain/cnn.com" "domain/aa" "kika/aaa"]) ;; => (true false true false false) ``` It is also possible to use a custom transformer instead of `string-transformer` (for example, in order to avoid `string-transformer` to perform additional encoding and decoding): ```clojure (def schema [:multi {:dispatch first :decode/my-custom #(str/split % #"/") :encode/my-custom #(str/join "/" %)} ["domain" [:tuple [:= "domain"] domain]] ["ip" [:tuple [:= "ip"] ipv4]]]) (def decode (m/decoder schema (mt/transformer {:name :my-custom}))) (decode "ip/127.0.0.1") ;; => ["ip" "127.0.0.1"] ``` ## Converting Schemas Example utility to convert schemas recursively: ```clojure (defn schema-mapper [m] (fn [s] ((or (get m (m/type s)) ;; type mapping (get m ::default) ;; default mapping (constantly s)) ;; nop s))) (m/walk [:map [:id :keyword] [:size :int] [:tags [:set :keyword]] [:sub [:map [:kw :keyword] [:data [:tuple :keyword :int :keyword]]]]] (m/schema-walker (schema-mapper {:keyword (constantly :string) ;; :keyword -> :string :int #(m/-set-properties % {:gen/elements [1 2]}) ;; custom :int generator ::default #(m/-set-properties % {::type (m/type %)})}))) ;; for others ;[:map {::type :map} ; [:id :string] ; [:size [:int {:gen/elements [1 2 3]}]] ; [:tags [:set {::type :set} :string]] ; [:sub [:map {::type :map} ; [:kw :string] ; [:data [:tuple {::type :tuple} ; :string ; [:int {:gen/elements [1 2 3]}] ; :string]]]]] ```