metosin / malli

High-performance data-driven data specification library for Clojure/Script.
Eclipse Public License 2.0
1.44k stars 204 forks source link

Unparsing map value by catn schema does not always keep entry positional order #925

Closed radhikalism closed 11 months ago

radhikalism commented 11 months ago

Steps:

Expected: (is (= V R))

Actual: (is (not= V R))

Example at JVM Clojure REPL:

> (let [schema-8 [:catn
                  [:a :int]
                  [:b :int]
                  [:c :int]
                  [:d :int]
                  [:e :int]
                  [:f :int]
                  [:g :int]
                  [:h :int]]
        schema-9 (into schema-8 [[:i :int]])
        input-8 [1 2 3 4 5 6 7 8]
        input-9 (into input-8 [9])]
    {:roundtrip-8 (->> input-8
                       (m/parse schema-8)
                       (m/unparse schema-8))
     :roundtrip-9 (->> input-9
                       (m/parse schema-9)
                       (m/unparse schema-9))})
;; result:
{:roundtrip-8 [1 2 3 4 5 6 7 8],
 :roundtrip-9 [5 7 3 8 2 4 6 9 1]}

Workaround using :cat + :orn:

> (let [schema-8 [:cat
                  [:orn [:a :int]]
                  [:orn [:b :int]]
                  [:orn [:c :int]]
                  [:orn [:d :int]]
                  [:orn [:e :int]]
                  [:orn [:f :int]]
                  [:orn [:g :int]]
                  [:orn [:h :int]]]
        schema-9 (into schema-8 [[:orn [:i :int]]])
        input-8 [1 2 3 4 5 6 7 8]
        input-9 (into input-8 [9])]
    {:roundtrip-8 (->> input-8
                       (m/parse schema-8)
                       (m/unparse schema-8))
     :roundtrip-9 (->> input-9
                       (m/parse schema-9)
                       (m/unparse schema-9))})
;; result:
{:roundtrip-8 [1 2 3 4 5 6 7 8],
 :roundtrip-9 [1 2 3 4 5 6 7 8 9]}

Comments

It seems to depend on the arbitrary order of the hash map that typically varies by its size, which suggests unparse applies to the seq of entries of map M. So small maps misleadingly seem to hold that (is (= V R)) by coincidence.

Other seqexp schemas like :* or :altn seem to preserve the order of the input value to unparse (rather than schema entry definition order), so maybe the order lossiness of :catn parse-unparse roundtrips is actually consistent in principle and should be expected.

If so, then it would still be interesting to have some other builtin schema to achieve the expected roundtrip behavior of this issue. Maybe a named-tuple or ordered-map type of schema (:catns?) as a terser form of the workaround above returning a MapEntry sequence preserving order.

ikitommi commented 11 months ago

Thanks for the detailed issue. I think we can use the schema entry keys to sort the result in unparse.

ikitommi commented 11 months ago

Should be fixed in master.