Welcome! Please see the About page for a little more info on how this works.

0 votes
in Spec by

When merging map specs with unqualified keys, only the last spec retains its conformation behavior:

(s/def ::int-or-string (s/or :int int? :str string?))
(s/def ::a ::int-or-string)
(s/def ::b ::int-or-string)
(s/def ::a-un (s/keys :opt-un [::a]))
(s/def ::b-un (s/keys :opt-un [::b]))
(s/def ::merge-un (s/merge ::a-un ::b-un))
(s/conform ::merge-un {:a 100 :b "foo"})
;;=> {:a 100, :b [:str "foo"]}

Compare the behavior with qualified keys:

(s/def ::a-qual (s/keys :opt [::a]))
(s/def ::b-qual (s/keys :opt [::b]))
(s/def ::merge-qual (s/merge ::a-qual ::b-qual))
(s/conform ::merge-qual {::a 100 ::b "foo"})
;;=> #:user{:a [:int 100], :b [:str "foo"]}

1 Answer

0 votes
selected by
Best answer

This is the expected behavior. s/merge does not "flow" conformed like s/and. You only get the conformed values per the last spec in the merge.

Then let's consider this a feature request (which I would understand not being considered until spec2). What I want here, and what feels to me like the intuitive behavior for an operation called "merge", isn't flowing so much as unioning. Flowing would run into trouble if the same `*-un` key showed up in both specs, or if a namespaced `::a` showed up in the input, since non-identity conformations are generally not idempotent – which means I can't necessarily fall back to `and`. As far as I can tell, there isn't currently any way to achieve this without either pulling apart the map spec or writing a custom implementation of Spec.
Spec 2 does have schema unioning.
Fabulous! Thanks for the answer.