malli 2025-05-29

Hi! First time posting in this channel … looking for constructive feedback and thoughts a new lib feature… I’m currently adding some additional functionality to the https://github.com/paintparty/bling library that will give users the ability to construct nice callouts based on Malli validation errors:

;; Currently on a feature branch with corresponding snapshot release
;; io.github.paintparty/bling {:mvn/version "0.7.0-SNAPSHOT"}

(require '[bling.explain :refer [explain-malli]])

(def Address
 [:map
  [:id string?]
  [:tags [:set keyword?]]
  [:address
  [:map
   [:street string?]
   [:city string?]
   [:zip int?]
   [:lonlat [:tuple double? double?]]]]])

(explain-malli
 Address
 {:id "Lillan",
 :tags #{:coffee :artesan :garden},
 :address
 {:street "Ahlmanintie 29", :zip 33100, :lonlat [61.4858322 87.34]}})
The above will result in:

❤️ 1
1

Another example, with additional options argument (turns off printing of the schema):

(explain-malli
 Address
 {:id "Lillan",
  :tags #{"coffee" :artesan :garden},
  :address
  {:city "Tempare" :street "Ahlmanintie 29", :zip 33100, :lonlat [61.4858322 87.34]}}
 {:display-schema? false})

👏 1
❤️ 2

This style seems to be much more verbose than humanize if there are multiple errors but maybe you could have best of both worlds if you inline the errors?

{:id "Lillan",
 :tags #{"coffee" :artesan :garden},
         ;^^^^^^^
         ;Must satisfy:
         ;  keyword?
 :address
  {:city "Tempare" :street "Ahlmanintie 29", :zip 33100, :lonlat [61.4858322 87.34]}}

Or even:

{:id "Lillan",
  :tags #{-----------
          | "coffee" | :artesan :garden},
<<<<<<<<<<------------
v :address
v {:city "Tempare" :street "Ahlmanintie 29", :zip 33100, :lonlat [61.4858322 87.34]}}
v 
v
Must satisfy:
 keyword?

The really tricky case which I don't think humanize does right is dealing with disjunctions. for example [:or :string :int] and [:and :string :int] humanize the same when passed nil https://malli.io/?value=nil&amp;schema=%5B%3Aor%20%3Astring%20%3Aint%5D

You get the same 2 :errors back from (m/explain [:or :string :int] nil) and (m/explain [:and :string :int] nil) but the pretty printers should really handle them differently.

I think for [:and :string :int] it should be something like:

nil
^^^

Must satisfy:
  :string 
and
Must satisfy:
  :int
And [:or :string :int] should be
nil
^^^

Must satisfy:
  :string 
or
Must satisfy:
  :int

One way to start approaching this is to group errors using their common prefix :path 's

hmmm good point it … it looks like I will need to do some grouping, esp with multiple problems on the same value e.g. (m/explain [:or :string :int] :foo)

You could even use some heuristics to cull branches. e.g.,

(explain [:or [:map [:a :int]] :boolean] {:a "a"})
doesn't need to mention it's not a boolean.

🙏 1

You can preview several examples of bling.explain/explain-malli http://malli.in your terminal with the following snippet:

clj -Sdeps '{:deps {io.github.paintparty/bling {:mvn/version "0.7.0-SNAPSHOT"}}}' -e "(require '[bling.sample]) (bling.sample/explain-malli-examples)"
In order to get the syntax coloring shown in the examples above, you need to export a BLING_MOOD env var with a value of "light" or "dark" . Otherwise the printing will default to a neutral theme with no colors. Docs here: https://github.com/paintparty/bling/tree/light-dark-color-variants?tab=readme-ov-file#usage-with-malli (edited)

• instead of (edn/read-string (with-out-str (prn (:schema problem)))) use (malli.core/form (:schema problem)) • for clean-schema see remove-swagger-keys for a better approach https://github.com/metosin/malli/blob/master/docs/tips.md#walking-schema-and-entry-properties

@ambrosebs Thank you! I now remember thinking “There’s gotta be a better way to do this with something in malli’s api” when I was implementing those bits^. I will use your suggestions 🙌

😁 1

That will help simplify walk-properties

🙏 1

It might end up looking something like:

(defn walk-properties [schema f]
  (m/walk
    schema
    (fn [s _ _ _]
      (mu/update-properties s f))
    {::m/walk-entry-vals true}))

Oh maybe it doesn't work with :map entry properties. e.g., [:map [:a {:prop :here} :foo]]. Try it out.

👍 1

now I remember, I think you should just use the documented way because there's a bug with ::m/walk-entry-vals in that it doesn't update its properties https://github.com/metosin/malli/pull/1091

👍 1