malli

Matti Uusitalo 2025-10-14T04:59:12.597889Z

Some news from Metosin open source team: We want to ensure the continuity of malli maintenance and to that end I have been named as main responsible person. I will be triaging new issues and PRs and handling releasing for the most part.

6
❤️ 3
💪 1
🙏 3
2025-10-14T11:19:45.157689Z

has tommi stepped back from maintenance or is he no longer with metosin? either way, thank you for taking over, i'm glad it's not been abandoned

Matti Uusitalo 2025-10-14T11:31:07.563319Z

Tommi is still with Metosin. I would say this means we have assigned clearer responsibilities and allocated time for the maintenance tasks.

👍 2
2025-10-14T12:11:37.270299Z

cool. thank you for the clarification

Samuel Ludwig 2025-10-14T13:59:21.972349Z

Wonderful news, Malli works so nicely/naturally it's one of those things that just feels like its a part of clojure.core for me.

ikitommi 2025-10-15T12:35:38.867529Z

Alive and kicking, but -30% time for OS just now.

1
❤️ 3
🫡 1
Matti Uusitalo 2025-10-14T05:32:46.575209Z

Releasing an alpha release for malli. We merged a PR that tightens up some previously unspecified behavior about transforming parsers. More information in the release notes and linked PR https://github.com/metosin/malli/releases/tag/0.20.0-alpha2 Clojars: https://clojars.org/metosin/malli/versions/0.20.0-alpha2 Looking forward to community feedback about the alpha. In our internal testing we have not noticed regressions.

🎉 2
ilmo 2025-10-30T07:12:58.649769Z

@matti.uusitalo117 This change seems to break metosin/oksa. I've posted an issue to its repository for further details: https://github.com/metosin/oksa/issues/27

👀 2
2025-10-30T14:26:04.387119Z

I've narrowed it down to (m/parser [:schema {:registry (oksa.parser/registry nil)} :oksa.parser/Arguments])

2025-10-30T14:30:15.397979Z

I'm guessing we need cycle detection like in malli.generators. I'll work on it.

👍 2
2025-10-30T14:42:01.598159Z

Here's the inlined version that blows the stack:

(m/parser
  [:schema {:registry
            {::Name [:or :keyword :string]
             ::Value [:or
                      number?
                      :string
                      :boolean
                      :nil
                      :keyword
                      [:sequential ::Value]
                      [:map-of ::Name ::Value]] 
             ::Arguments [:map-of ::Name ::Value]}}
   ::Arguments])

2025-10-30T14:44:13.992599Z

minimal failure:

(is (m/parser
      [:schema {:registry
                {::Value [:sequential [:ref ::Value]]}}
       ::Value]))

2025-10-30T17:34:54.006499Z

fixed it, both schemas are inferred as having simple parsers, and it resolves the oksa issue. I'll add a bunch of interesting unit tests with strange cycles and send a pr.

👏 1
🚀 1
🎉 1
2025-10-30T18:49:21.419429Z

a nice abstraction emerged from using the same principle to detect cycles as generators, I'm sure it's useful in other places like tying the knot for validators. the problem is -validator doesn't take an opts arg 😕 here's the pr https://github.com/metosin/malli/pull/1234

👀 1
2025-10-30T19:17:39.438229Z

Here's a sketch of my idea for tying the knot for :ref validators: https://github.com/metosin/malli/pull/1235

yuhan 2025-10-14T15:30:01.237339Z

Should the :or schema also disallow multiple transforming parsers? Otherwise you'd get ambiguous parses like

(m/parse [:or
          [:orn [:x :int] [:y :boolean]]
          [:orn [:x :boolean] [:y :int]]]
         123)
;; => #malli.core.Tag{:key :x, :value 123}

🤔 1
2025-10-14T16:26:39.258949Z

The main problem with :and is its "flowing" behavior of the parsed value going from left to right. in [:and [:orn ..] S] S would receive the parsed value of :orn to parse, instead of the original value. This doesn't really make sense. OTOH :or is unambiguous in this respect. I think you bring up a good point that attempting to unparse this :or would be ambiguous.

2025-10-14T16:27:35.460669Z

If you were thinking about unparsing, could you find a concrete example of it breaking unparsing?

2025-10-14T16:29:49.697519Z

It may just be the nature of :or that it works poorly with unparsing. If you really cared about it, you'd use :orn today.

2025-10-14T16:30:48.155659Z

maybe there's a third schema we can think about [:orpos ..] which parses schemas using positional tag keys, or even [:or {:parse/mode :positional} ..] , the default being :transparent mode. IIRC this really crystallizes an important deviation with spec's design, that everything is tagged. This is the consequence AFAICT.

2025-10-14T16:32:26.962389Z

but we can't really change the format of :or's default parsed value without disruption. ditto restricting its parsing to only schemas that unambiguously unparse.

2025-10-14T16:32:52.568079Z

IMO. would love to hear your thoughts @qythium

yuhan 2025-10-14T17:34:10.735029Z

yeah I can't think of a 'realistic' example offhand, the above is of course really artificial but it kinda makes me wary about the compositional soundness of the whole parsing system in the large

yuhan 2025-10-14T17:53:32.005239Z

🤔

(let [schema [:or [:fn record?] [:orn [:i :int]]]]
  (->> 123
       (m/parse schema)
       (m/unparse schema)))
;; => #malli.core.Tag{:key :i, :value 123}

2025-10-14T19:17:59.413319Z

Yes, maybe there's something we can do about that at least in terms of removing the footgun. We could have an extensible linter to check if your schema is fully roundtrip-able. For :or that contain overlapping schemas, we return false. So if you're doing real work with parsing, you'd assert that check before you m/parse anything.

yuhan 2025-10-14T20:45:04.190419Z

is it overly conservative on all regex schemas by design treating them as always transforming? I realise this can be handled with the :parse/transforming-child 0 prop

(m/parse [:and
          [:orn [:v vector?] [:l list?]]
          [:cat :keyword [:* :any]]]
  [:x])
;; => Execution error (ExceptionInfo) at malli.core/-exception (core.cljc:203).
;;    :malli.core/and-schema-multiple-transforming-parsers

2025-10-14T20:46:13.674449Z

I don't think I gave much thought to regex schemas.

2025-10-14T20:47:19.375199Z

so no, that's open to suggestions

2025-10-14T20:56:24.075289Z

Which regexes are actually transforming?

2025-10-14T22:30:46.225779Z

Here's the linter I proposed, it detects your example using record? and seems pretty accurate https://github.com/frenchy64/malli/pull/38

2025-10-14T22:33:15.700589Z

It actually leverages -parser-info so that's a point to it being a good abstraction. I'm exploring using malli schemas to write Typed Clojure typing rules and I think I'm going to use this to force users to only use schemas with roundtripping parsers.

ilmo 2025-10-31T07:09:38.818929Z

Yup, can double-confirm malli master now works against oksa. Thanks @ambrosebs!

🙌 1
🎉 1
Matti Uusitalo 2025-10-31T07:18:45.143649Z

Thanks for the report @ilmo.raunio

🙇 1
yuhan 2025-10-17T12:05:04.190969Z

yeah, apologies for somewhat derailing the conversation :P I haven't actually used m/parse in production (mostly validate/coerce) so the above was all theoretical edge-case finding, would be great if others with real experience could chime in. I did find the behaviour of -parser-info somewhat unintuitive (how it appeared to 'drop information' from child schemas by returning nil) - but I guess that's an internal API detail users shouldn't have to interact with.

2025-10-17T17:54:16.688289Z

@qythium fair point on -parser-info. I was a bit vague on what nil means, the way I think about it is there's 3 values of (:simple-parser (-parser-info s)): • true (non-transforming parser) • false (always-transforming parser) • nil (unknown) I conflated the last two because I couldn't motivate distinguishing them.

yuhan 2025-10-16T04:27:30.585329Z

hmm I just had a look at this, not quite convinced that it's worth the engineering effort to prevent such edge cases - after all I don't think Malli isn't trying to be a completely sound type system in the formal sense

✅ 1
yuhan 2025-10-16T04:34:09.919739Z

eg here's another constructed example

(m/parse :keyword :malli.core/invalid)
;; => :malli.core/invalid
is that a passing or failing parse? Malli uses social namespacing conventions to say there's a 99.9% chance that users won't be manipulating this specific keyword in their domain, I think that's a reasonable tradeoff but it means that you sacrifice 'correctness' on such gotchas like
(m/parse [:tuple :int :keyword] [1 :malli.core/invalid])
;; => :malli.core/invalid

(m/unparse [:tuple :int :keyword] [1 :malli.core/invalid])
;; => :malli.core/invalid

yuhan 2025-10-16T04:47:58.324759Z

re regex schemas, I think it's also reasonable to treat them as always-transforming, even the versions without -n because of what happens when they're nested:

(m/parse [:cat :int [:cat :int]]
  [1 2])
;; => [1 [2]]
which.. if you squint a little lets you construct all kinds of non-injective parses which violate the rountrip property:
(map (m/parser [:cat :int [:? [:maybe :int]] :int])
  [[1 2 3]
   [1 3]
   [1 nil 3]])
;; => ([1 2 3] [1 nil 3] [1 nil 3])

yuhan 2025-10-16T04:56:03.612059Z

I do think this one's a bit more significant than the above one, returning nil as a Nothing sentinel feels a little iffy given how prevalent nils are in actual data (vs the m/invalid)

yuhan 2025-10-16T05:07:03.757889Z

to be a little more realistic:

(def Hiccup ; (leaf node)
  [:cat :keyword [:? [:maybe [:map-of :keyword :any]]] [:* string?]])

(m/parse Hiccup [:div "edge" "case"])
;; => [:div nil ["edge" "case"]]

(let [v [:div "edge" "case"]]
  (= v (->> v (m/parse Hiccup) (m/unparse Hiccup))))
;; => false

yuhan 2025-10-16T05:13:07.083259Z

Here's one that doesn't involve nils / maybes:

(let [s [:cat :keyword [:alt [:vector :any] [:+ :int]]]
      v [:k 1 2 3]
      roundtrip #(->> % (m/parse s) (m/unparse s))]
  (list v (roundtrip v)))
;; => ([:k 1 2 3] [:k [1 2 3]])

2025-10-16T13:43:12.645089Z

Parsing regexes is weird! :? especially seems either broken or needing a tagged counterpart.

2025-10-16T13:46:43.072099Z

(m/parse :keyword :malli.core/invalid)
;; => :malli.core/invalid
I guess this one still roundtrips tho? To distinguish the two cases you'd use a singleton :orn I think.

2025-10-16T13:47:43.335009Z

Oh, did you conclude this was actually a failing parse?

yuhan 2025-10-16T13:49:53.176619Z

yeah check out the following tuple thing, the impl basically treats any sub-parser return value of m/invalid as a monadic failure and threads that through to the top

👍 1
2025-10-16T13:51:31.413089Z

Since there's no subparser in this case, is this a successful parse?

🤷‍♂️ 1
2025-10-16T13:56:55.089249Z

Is the :+ unparser broken? Why doesn't it unwrap the vector?

2025-10-16T13:57:33.282549Z

or is that because of :alt?

yuhan 2025-10-16T13:58:12.072369Z

that's a useful feature actually - it means you can destructure the parsed value according to the nested structure of the defining schema, rather than the flat input

2025-10-16T13:59:25.058019Z

IIUC that's the parser, I was confused that the unparser wasn't firing, but then I realized you constructed the :alt such that it used :vector for the unparser.

yuhan 2025-10-16T14:00:10.481339Z

yeah exactly, messing around with the asymmetry

yuhan 2025-10-16T14:00:23.454969Z

different clauses being activated on parse vs unparse

2025-10-16T14:01:01.343139Z

does that mean :alt is has similar rules to :or in terms of roundtripping?

yuhan 2025-10-16T14:05:41.170509Z

hmm it seems strange to me to use :alt outside of an enclosing :cat , although I guess that's totally valid - really haven't used these regex schemas much in practice. if you really wanted guaranteed rountrippability then probably the thing to do is use the -n versions, though that means desugaring [:? s] into something like [:altn [:zero [:cat]] [:one [:cat s]]] and similarly for [:+ s] into [:catn [:head s] [:tail [:* s]]

2025-10-16T14:08:04.644279Z

seems analogous to the situation with :or/`:orn`. I like the tip about :? tho, might be useful in the readme about general tips for robust roundtripping.

yuhan 2025-10-16T14:16:21.921589Z

I do wonder what the practical upshot of guaranteeing 'true' roundtrippability is besides satisfying oneself of the unp ∘ p ≅ id 'law' - in the hiccup example above the output is actually in a sense isomorphic / 'normalized' version of the original in the sense of what it denotes? Agree that the regex ones might need a bit of closer look though, I can imagine some variation of my examples above making it into a real world bug in a much subtler / hard to trace form

2025-10-16T14:18:51.380249Z

Well the original problem was to make -parser-info more precise for as many schemas as possible so the new parser for :and causes minimal disruption.

yuhan 2025-10-16T14:19:31.052409Z

yeah my gut feeling is that you'd have to to do a ground-up breaking change to solve this in fuller generality, in malli idiom something like (defprotocol Parsed (-ptype [_]) (-pval [_])), and then a (parse :int 5) would return something like (reify Parsed (-ptype [_] :int) (-pval [_] 5)) - though then the ergonomics get a lot worse

2025-10-16T14:20:05.164769Z

can regex parsers be unparsed? don't they nest?

2025-10-16T14:21:00.791529Z

> ground-up breaking change FWIW the whole point behind my linter idea is that we can't fix this. We can only try and detect if the subset of schema's we're using roundtrip. That's a useful idea to me, sometimes it's mission-critical. Most of the time, you never unparse.

2025-10-16T14:23:56.307959Z

One of the really disappointing things about spec for me is that you can't use it to manipulate programs using associative structures in practice, since you always lose some information during unconforming. This is one usecase for thinking about this problem systematically. You could then go even further, by preserving metadata.

2025-10-16T14:26:00.929629Z

> can regex parsers be unparsed? don't they nest? I think they do unparse, the examples above are just edge cases where the unparsing logic has been hijacked. like in [:alt A B] you parse with B then unparse with A.

2025-10-16T14:26:57.584479Z

ah yeah, looking at the code, there's X-unparser for all of them

2025-10-16T14:26:59.943039Z

the implicit lesson here is always use tagged schemas for parsing if you intend to unparse.

yuhan 2025-10-16T14:39:32.622199Z

hmm I've never really used spec conform/unform in practice, could you give an example of what that 'information loss' looks like? I suppose you're thinking in terms of using it as bidirectional lens for updating some nested part of a structure - curious actually where unparse/unform gets actually used in real world scenarios

2025-10-16T14:43:03.043219Z

Something like a set losing its sortedness or a list turning into a vector. Some information is just lost. It's been probably a decade since I really tried it, but you can find historical reports of people using the core specs for destructuring etc to manipulate syntax and not having success.

2025-10-16T14:45:02.102819Z

My frustration was more than the general idea of robust roundtripping was not a big a focus as I'd hoped.

2025-10-16T14:45:29.101829Z

update-in is so much nicer than a huge positional destructure with an apply list...

2025-10-16T14:48:47.051389Z

I think -parser-info opens up a lot of doors for tooling in this direction, that just isn't there in spec. Especially if the community implements extends their own schemas. So while Malli roundtripping is much less robust, I have hope that this problem can be addressed, even outside of malli itself, for people who want it.

2025-10-16T14:54:49.548229Z

(that being said, I don't know if -parse-info is really needed in spec because IIRC it prefers tags for conforming)

2025-10-16T14:56:38.025989Z

In terms of action items, I think we could improve the inference of the transforming spec for schemas like [:and [:alt ..] ..] and [:and [:cat ..] ..] in some cases. People do get :alt mixed up with :or and the :cat might be useful in the real world.

2025-10-16T14:57:11.212499Z

I think there's one in the readme like [:and [:cat ..] vector?] for improving the generator.

yuhan 2025-10-16T15:01:50.631139Z

yeah I think it would be nice if :cat took a property to say whether it should match against lists/vectors/other(?) , the current way of saying [:and .. vector?] (and then having to manually add a :gen/fmap vec for generative testing) always felt like a bit of a kludge

2025-10-16T15:04:26.510639Z

Yes I have a solution for that in my long long backlog. Maybe next year.

2025-10-16T15:04:50.385579Z

But your solution is much more straightforward 🙂

2025-10-16T15:05:09.280809Z

What's a good name for such a property?

2025-10-16T15:06:35.946529Z

feeling something like :gen/into []

2025-10-16T15:07:49.853079Z

Could also create a :catv schema.

2025-10-16T15:08:43.195089Z

I can't think of any other base types you'd need. I think :cat uses seqs by default.

yuhan 2025-10-16T15:14:24.316379Z

it's something I was planning to put a bit more thought into as well - wondering if it could be unified with another friction point I've noticed with [:multi {:dispatch first}] - or any other partial predicate - there doesn't seem to be any clean way of cleanly telling it to only match sequential things besides [:and sequential? :multi..] and even then it would throw an error on m/explain - thinking aloud both seem to point to a sort of :pre -like invariant , like a subschema that does double duty as a precondition for validating/parsing and post-transformation for unparsing/generating?

2025-10-16T15:16:26.278139Z

should :and distribute over :multi?

2025-10-16T15:17:15.372719Z

Oh are you worried about guarding the dispatch function from getting bad input?

yuhan 2025-10-16T15:19:32.460239Z

yeah, for example

(m/explain [:and vector? [:multi {:dispatch first}
                          [:k [:tuple :keyword :any]]]]
  :oops)
;; => Execution error (IllegalArgumentException) at malli.core/-multi-schema$reify$reify$fn (core.cljc:1870).
;;    Don't know how to create ISeq from: clojure.lang.Keyword

yuhan 2025-10-16T15:20:16.756739Z

(although this is maybe getting a bit off-topic from the original thread)

😄 1
✅ 1
2025-10-16T15:21:29.621389Z

nah I appreciate wanting to get maximum value from new abstractions!

2025-10-16T15:23:35.111889Z

I bet that could be automatically generated by finding the commonalities between all branches.

2025-10-16T15:24:09.088089Z

brings up a lot of questions tho.

2025-10-16T15:26:36.244549Z

I think this :multi generates just fine, it's probably onto the user to have the dispatch function cover all vals (I'm surprised :multi doesn't wrap a catch implicitly). So yeah maybe a different axis of improvement than :catv.

2025-10-16T15:28:02.287419Z

I think :catv has other uses too, it's basically :tuple that supports regexes.

2025-10-16T15:30:24.695429Z

not a great name tho since it's not a regex. should be more like :tuple-regex or :vcat

yuhan 2025-10-16T15:35:39.479109Z

i think :variant is a nice term for it - maybe a bit specific though I'm thinking of the tagged-variant idiom for sum types that malli doesn't seem to have a nice way of speccing, probably something that would live in malli.util though edit: will flesh that last bit out and start a new thread - maybe there's just something I'm missing

👍 1
2025-10-16T15:44:49.459679Z

ok so I think simply reusing the same logic of :or in :alt's -parser-info is the lowest hanging fruit from this discussion.

2025-10-16T15:45:35.080159Z

and perhaps :tuple's in :cat.

2025-10-16T15:47:05.471959Z

and leave :? :+ and :* as transforming.

2025-10-16T15:47:59.903529Z

ah but only top-level regexes?

yuhan 2025-10-16T15:48:06.418499Z

it's a bit different though, I think alt and cat are non-transforming only if their children are all non-regex schemas - see the [:cat [:cat ]] example

2025-10-16T15:48:50.512949Z

Great that still sounds achievable to implement.

👍 1
yuhan 2025-10-16T15:50:31.785469Z

(don't take my word for it though haha, I've taken a look at the regex impl and have no idea how its crazy CPS stuff works)

😄 1
2025-10-16T15:51:29.573269Z

they wrote a great blog post about it here: https://www.metosin.fi/blog/malli-regex-schemas

2025-10-16T15:51:53.483739Z

summarized https://github.com/metosin/malli/issues/1230

yuhan 2025-10-16T15:52:38.613149Z

yep even after reading that blog - the actual productionized/optimized impl turned out quite a bit different

👍 1
2025-10-16T15:53:01.906589Z

@ambrosebs "`:cat` is like :cat"

👏 1
2025-10-16T15:53:37.309229Z

catjam

2025-10-16T15:53:47.955789Z

I submitted a bugfix to the regex impl and I still have no idea how it works.

😲 1
2025-10-16T15:54:45.794399Z

I'm sure there was a fleeting moment when I thought I understood.

Matti Uusitalo 2025-10-17T04:53:40.620619Z

Reading through these comments in the context of conversation about the alpha, this is mostly "yes and we might want to look at these things in the future" comments? I didn't see "no that's breaking my workflows" or "no that will be bad going towards the future" comments.

Samuel Ludwig 2025-10-14T18:12:52.087589Z

Vocabulary question For the following map:

(def ex-schema
  (m/schema
    [:map
     [:id [:int {:min 1}]]
     [:name :string]
     [:email {:optional true} email?]
     [:phone {:optional true} phone-number?]]))
Is there any terminology you would use to differentiate between the properties of the schema which is the value of the :id child (`{:min 1}`), and the properties of each of the children of the :map schema (`{:optional true}`) I want to call both the :min 1 and :optional true "child properties", but that's ambiguous

2025-10-14T22:40:30.476079Z

Those children are nested at different levels. The :map has entry schemas like :id. The entry schema has a key, props, and value schema part. The value schema then can have its own props.

2025-10-14T22:43:12.377759Z

So :map contains the`:id` entry schema which contains the`:int` value schema.

2025-10-14T22:43:24.893199Z

Not sure if that helps.

Matti Uusitalo 2025-10-15T11:05:18.162059Z

To my knowledge we just refer to them as properties, but I see your point. In the :int spec we have a property that defines details about the value. In the case of :optional key it controls if the key is required in the map. So there is a difference

❤️ 1
ikitommi 2025-10-15T12:40:27.486599Z

(map) entry properties

❤️ 1
Samuel Ludwig 2025-10-15T14:03:12.762129Z

I appreciate the clarifications everyone gratitude Sticking to the calling the map's children "entries" helps, as I think calling them "entry schema" is a bit of a misnomer (calling m/schema and m/properties on [:email {:optional true} email?] would fail (which actually inspired this question!)). Helps me keep a better picture :^)