malli 2026-07-10

Say I have a schema like this:

(def ^:private Config
  [:map
   [:runtime-config [:map
                     [:authentication
                      [:map
                       [:show-request-cookies {:default false} [:enum true false]]]]
                     [:caches
                      [:map
                       [:websockets
                        [:map
                         [:ttl-in-milliseconds {:default 300000} pos-int?]]]]] ;; 5 minutes
                     [:metrics
                      [:map
                       [:prometheus
                        [:map
                         [:enabled {:default false} [:enum true false]]]]]]     
                     [:sentry [:map [:dsn [:maybe :string]]]]]]])

(sorry for the delay, we've all been on vacation pretty much) that's a really good question! the problem kinda is that if :caches is missing, malli won't recurse inside it and apply the decoder

I find that if I use (m/decode Config ,,,, mt/default-value-transfomer), I don't get default values for the maps unless I also do this:

(def ^:private Config
  [:map
   [:runtime-config [:map
                     [:authentication
                      [:map {:default {}}
                       [:show-request-cookies {:default false} [:enum true false]]]]
                     [:caches
                      [:map {:default {}}
                       [:websockets
                        [:map {:default {}}
                         [:ttl-in-milliseconds {:default 300000} pos-int?]]]]] ;; 5 minutes
                     [:metrics
                      [:map {:default {}}
                       [:prometheus
                        [:map {:default {}}
                         [:enabled {:default false} [:enum true false]]]]]]
                     [:sentry [:map [:dsn [:maybe :string]]]]]]])

Note the {:default {}} against the`:map` entries

For a small config, like this, it's okay(ish), but imagine a bigger config with lots of nested maps, where I define default values. Having to put {:default {}} against the parent(s) key(s) starts to look like noise.

Have I misunderstood something, about how to get the maps to have defaults when "applying-defaults" in the (m/decode ....)?

(sorry for the delay, we've all been on vacation pretty much) that's a really good question! the problem kinda is that if :caches is missing, malli won't recurse inside it and apply the decoder

what my past projects have tended to do is to have the default config as a large literal or edn file, which then gets deep-merged with the decoded config

but I see the appeal of documenting the defaults in-line

the :default {} workaround could be documented better, for sure

That's awesome, but also a bit clumsy, as if I have a big config map, I would have to mark each one, that should have defaults with the :default {} attribute. Doable, of course, but sorta adds noise to the map.

Could it not be made, so that if (m/decode Config config mt/default-value-transformer)) is invoked, it would walk the config and apply the defaults, no matter how deeply those defaults are nested?

(and I Hope you had a nice holiday! :-))

Since malli schemas are data, you can also write your own add-default-to-every-submap function

A workaround still, but less of a manual one

I wonder if we have an issue about this thing already...

I could, but would it not be more approriate for malli to do that for me, if I use the {:default xyx} attribute? It does feel suprising to the end user, that whilst malli supports default values, it's left to the user to actually do that for something that is nested.

(hmm, can't find an existing issue)

Yeah, I totally agree that it is not good UX!

I appreciate the consideration 🙂

I'm a bit hesitant to touch the default-value-transformer since it's a heavily-used part of malli and the risk of regressions for complex schemas seems pretty high. I guess we could have a separate recursive-default-value-transformer or something, at least at first, as an experiment...

Don't mind using a different transformer if one is provided for me 🙂 Happy to help 🙂

Hmm... it looks like

(mt/default-value-transformer {:defaults {:map (constantly {})}})
could do the trick!

if so, it should be documented in the README

Let me try that 🙂

on an existing config

Seems to work

awesome sauce

by doing that, it's actually helped me to notice where I've placed some of my {:defaults ,,,,} in the wrong place

Document document document! 😄

I'll add a reminder about that!

🎉 1

Thank you for getting back to me on this 🙂