Using Schema-like schemas in malli

Thanks for sharing! Malli (and spec-tools) lead here. Agree that data-specs are handy when used with libs like reitit, as long as the needs are simple.

Suggestions:

  • It might be better not to support automatically interpreted vectors or map-of’s to avoid ambiguity. simple syntax should be simple
  • by default, meta-data is not visible when printing the form, so debugging the syntax is harder as one can’t know which form has the ^:m
  • like data-specs, adding functional helpers for optional keys, maybes, and ors might be a good idea.

Here is an alternative implementation:

(ns malli.experimental.lite
  (:refer-clojure :exclude [set vector and or])
  (:require [malli.core :as m]))

(declare schema)

(defrecord -Optional [value])
(defn -schema [t & xs] (schema (into [t] (map schema xs))))
(defn -entry [[k v]]
  (let [[v optional] (if (instance? -Optional v) [(:value v) true] [v])]
    (cond-> [k] optional (conj {:optional true}) :always (conj (schema v)))))

(defn schema [x] (m/schema (if (map? x) (into [:map] (map -entry x)) x)))

(defn optional [x] (->-Optional x))
(defn maybe [x] (-schema :maybe x))
(defn set [x] (-schema :set x))
(defn vector [x] (-schema :vector x))
(defn map-of [k v] (-schema :map-of k v))
(defn tuple [& xs] (apply -schema :tuple xs))
(defn and [& xs] (apply -schema :and xs))
(defn or [& xs] (apply -schema :or xs))

defining schemas:

(require '[malli.experimental.lite :as l])

(def Schema
  (l/schema
   {:map1 {:x int?
           :y [:maybe string?]
           :z (l/maybe keyword?)}
    :map2 {:min-max [:int {:min 0 :max 10}]
           :tuples (l/vector (l/tuple int? string?))
           :optional (l/optional (l/maybe :boolean))
           :set-of-maps (l/set {:e int?
                                :f string?})
           :map-of-int (l/map-of int? {:s string?})}}))
;[:map
; [:map1
;  [:map
;   [:x int?]
;   [:y [:maybe string?]]
;   [:z [:maybe keyword?]]]]
; [:map2
;  [:map
;   [:min-max [:int {:min 0, :max 10}]]
;   [:tuples [:vector [:tuple int? string?]]]
;   [:optional {:optional true} [:maybe :boolean]]
;   [:set-of-maps [:set [:map [:e int?] [:f string?]]]]
;   [:map-of-int [:map-of int? [:map [:s string?]]]]]]

works normally with rest o malli:

(require '[malli.core :as m])
(require '[malli.error :as me])

(-> Schema
    (m/explain
     {:map1 {:x 1
             :z "kikka"}
      :map2 {:min-max 12
             :tuples [[1 "a"] [2 "b"]]
             :set-of-maps #{{:e 1, :f "f"}}
             :map-of-int {1 {:s "seppo"}
                          2 {:s :sanni}}}})
    (me/humanize))
;{:map1 {:y ["missing required key"]
;        :z ["should be a keyword"]}
; :map2 {:min-max ["should be between 0 and 10"]
;        :map-of-int {2 {:s ["should be a string"]}}}}

What do you think?

EDIT: I pushed the key optionality into value, instead of special wrapper for key (l/opt, l/req like in data-specs), I think it’s simpler that way, yup does it like this too,

3 Likes