clojure-spec

pieterbreed 2026-02-03T09:27:06.282829Z

Good morning everyone 👋🏽 I think I've found a bug in spec, and before I submit it, would appreciate if someone could look over my shoulder and educate me if the bug is in my thinking... Specifically, this is about validating a value against a spec defined as (s/and :spec/one :spec/two) First, an example that works like how I expect.

;; expected behaviour
  (s/def :working.test1/one string?)
  (s/def :working/test1
    (s/keys :opt [:working.test1/one]))

  (s/def :working.test2/two number?)
  (s/def :working/test2
    (s/keys :opt [:working.test2/two]))

  (s/def :working/test
    (s/and :working/test1
           :working/test2))

  (def test-value
    {:working.test1/one "a string"
     :working.test2/two 1.23456})

  (s/valid? :working/test1
            test-value) ;; true
  (s/valid? :working/test2
            test-value) ;; true
  (s/valid? :working/test
            test-value) ;; true
and here, in combination with one of the and'ed specs being itself defined with s/or , an example that behaves unexpectedly:
;; unexpected behaviour
  (s/def :broken.test1/one
    ;; this s/or is required to trigger the unexpected behaviour
    (s/or :s string?
          :n number?))
  (s/def :broken/test1
    (s/keys :opt [:broken.test1/one]))

  (s/def :broken.test2/two
    number?)
  (s/def :broken/test2
    (s/keys :opt [:broken.test2/two]))

  (s/def :broken/test
    (s/and :broken/test1
           :broken/test2))

  (def test-value-2
    {:broken.test1/one "one"
     :broken.test2/two 123})
  (def test-value-3
    {:broken.test1/one 1.0
     :broken.test2/two 123})

  (s/valid? :broken/test1
            test-value-2) ;; true
  (s/valid? :broken/test1
            test-value-3) ;; true

  (s/valid? :broken/test2
            test-value-2) ;; true
  (s/valid? :broken/test2
            test-value-3) ;; true

  (s/valid? :broken/test
            test-value-2) ;; false ;; wait, what?!
  (s/valid? :broken/test
            test-value-3) ;; false ;; !!!

pieterbreed 2026-02-03T09:30:45.617049Z

With a little bit of debugging on my side, it appears that the value being validated (at some point) becomes the conformed version of the value; IE [:s "one"] is not valid against :broken.test1/one

lassemaatta 2026-02-03T09:31:47.685749Z

I think "Returns a spec that returns the conformed value." is relevant here

👍🏽 1
lassemaatta 2026-02-03T09:33:51.308249Z

also, https://ask.clojure.org/index.php/11230/why-does-s-conform-transform-data discusses this I think

pieterbreed 2026-02-03T10:03:05.603229Z

Thank you for the link; It appears to describe nearly the same situation as what I've posted here. I don't understand Alex's reasoning there though, flowing vs non-flowing, I'm not sure what to make of it. But I guess the statement > There doesn't seem to be a good mechanism to express "this value satisfies a series of spec simultaniously" still holds true.

pieterbreed 2026-02-03T10:10:46.555339Z

I see the docs for s/and is actually clear about this: > Returns a spec that returns the conformed value. Successive > conformed values propagate through rest of predicates. 😢

lassemaatta 2026-02-03T10:15:21.873329Z

I vaguely recall seeing an example somewhere where people worked around this by supplying a custom conformer for the first spec. So you could specify a :broken.test1/one-non-conforming or something like. Not sure if this is a good idea though.

👍🏽 1
Alex Miller (Clojure team) 2026-02-03T23:22:57.126659Z

one tool you can use here is the (undocumented) s/nonconforming which can be wrapped around the s/or spec in :broken.test1/one to change the s/or conforming behavior

Alex Miller (Clojure team) 2026-02-03T23:24:19.932589Z

(s/def :broken.test1/one (s/nonconforming (s/or :s string? :n number?)))
or alternately, you could wrap that in the :broken/test s/and, depending on what you want to happen during conform of :broken.test1/one

Alex Miller (Clojure team) 2026-02-03T23:26:53.535999Z

this is a case where flowing conformed values through s/and yields confusing results. there are other cases where flowing the conformed values is a useful tool though (and really in hindsight I think non-flowing would be a better default behavior for s/and, although changing that would break a lot of things now)

pieterbreed 2026-02-04T05:57:19.648109Z

Thank you for the explanation and the hint of s/nonconforming 🙏🏽