Hi, does anyone know if there is there an idiomatic way to validate a map where:
1. Each entry must conform to one of N specific k/v pairings
2. m/explain produces a path to the specific offending entry, and/or offending value within that entry?
(require '[malli.core :as m])
;; A map where each entry must be one of these valid k/v pairings:
;; keyword/int
;; number/string
;; string/map
;; We want to validate a map like:
{12 "hello"
:z 42
"a" 1}
(def map-entry-schema
[:orn
[:foo [:tuple keyword? int?]]
[:bar [:tuple number? string?]]
[:baz [:tuple string? map?]]])
;; Works fine at the level of map-entry
(m/validate map-entry-schema [12 "hello"]) ;=> true
(m/validate map-entry-schema [:z 42]) ;=> true
(m/validate map-entry-schema ["a" 1]) ;=> false
;; The problem: how to lift this to map level with `m/explain` support?
;; Option A: :fn + every? — enforces pairing but `m/explain` gives no path to bad entry - the path is to the whole map
(def map-schema-a
[:and
:map
[:fn #(every? (fn [entry] (m/validate map-entry-schema (vec entry))) %)]])
;; Option B: :map-of with :or — gives explain paths but loses pairing
;; enforcement, so would result in false positives
(def map-schema-b
[:map-of
[:or :int :keyword :string]
[:or :string :int :map]])
(m/validate map-schema-b {:12 "hello" :bar-key 42}) ;=> true
(m/validate map-schema-b {:foo-key 42 :bar-key "oops"}) ;=> true ; not actually true, pairing not enforcedOh yeah I do remember seeing that malli.experimental.validate was added a few months ago, thanks for the reminder I will experiment with that as well.
:dmap-of looks awesome! Thank you for drafting that I will definitely be utilizing it
I have no experience implementing -transformer so I left that as the big TODO.
yeah transformers are tricky … more than meets the eye
This comment from the draft piqued my interest:
> ;; note: just like :map-of, we have :in == [“a”] here, even though we blame the key
Is there currently any way to reliable know if the :value entry in an error map is a map key or not? I’ve been under the assumption that the answer is no, and I’ve been using a custom function (with the result of m/explain ) to highlight offending key values in maps (for custom warning callouts):
(defn- target-key?
"Takes an error map from malli.core/explain result and the value that produced
that result, and determines if the `:value` in the error map is a map key."
[{:keys [value in path] :as error}
x]
(boolean
(when (coll? x)
(let [vectorized (walk/postwalk #(if (seq? %) (vec %) %) x)
m (->> in drop-last (get-in vectorized))
k (when (map? m) (->> in last (find m) first))]
(and (= k value)
(not= 1 (last path)))))))
(let [x {"a" "a"}]
(-> (m/explain [:map-of :int :string] x)
:errors
first
(target-key? x)))
;; => true
;; example with more complex nesting
(let [v [{:a {"a" "a"}}]]
(some-> (m/explain [:vector [:map-of
:keyword
[:map-of :int :string]]] v)
:errors
first
(target-key? v)))
;; => true
(let [v [{:a {1 :a}}]]
(some-> (m/explain [:vector [:map-of
:keyword
[:map-of :int :string]]] v)
:errors
first
(target-key? v)))
;; => false
The approach above has seemed to work ok so far, but of course I’m not sure about different edge cases as I’m still very much a malli noob. If there is in fact a reliable way to determine this, it would be cool if this was included in the error map from m/explain:
{:path [0]
:in ["a"]
:schema malli.core$_simple_schema$reify$reify__10018@3402b508
:value "a"
:map-key? true}
I have similar questions after this exercise. The :value was sometimes set to the val even tho the key was to blame.
I guess the (peek path) will tell you whether the :value matters or not.
if it's 0 then just use (peek in) to find the problematic value.
I opened this related issue about humanization in this case https://github.com/metosin/malli/issues/1290
Happy to see there is a fresh issue on the humanization part.
wrt to peek thank you that is a good idea … I will use that to refactor my clumsy target-key fn haha.
I think humanize has more support for this sort of thing. See :error/path https://github.com/metosin/malli#custom-error-messages
I drafted a schema that mostly works as you specified here https://github.com/frenchy64/malli/pull/54/changes
FWIW I think this is beyond :map and :map-of. You'd need to create your own schema.