UPDATE:
Now I have published a library to handle this, please check it out at: https://github.com/wilkerlucio/spec-coerce
You can use spec for coercion, but it's important that you also have the non-coercive version of it. If you force the coercion on your specs you are doing 2 things at the same time, violating SRP. So the recommendation is having a simple validation one, and then you can another one on top of it, so later you can choose if you want to use the coercive version or the simple validation one.
Another option (that I prefer) is to have a coercion engine based on the specs running in parallel. If you look at how specs infer the generator from the specs (check here), you can see that you can use the spec form to derive something else, so this something else can be your coercion engine.
I have written one article where I explain how to do that, you can find it here (just jumping to the coercing with specs section): https://medium.com/@wilkerlucio/implementing-custom-om-next-parsers-f20ca6db1664
Code extracted from the article for reference:
(def built-in-coercions
{`int? #(Long/parseLong %)
`nat-int? #(Long/parseLong %)
`pos-int? #(Long/parseLong %)
`inst? clojure.instant/read-instant-timestamp})
(defn spec->coerce-sym [spec]
(try (s/form spec) (catch Exception _ nil)))
(defn coerce [key value]
(let [form (spec->coerce-sym key)
coerce-fn (get built-in-coercions form identity)]
(if (string? value)
(coerce-fn value)
value)))
Also here is a more elaborated version of it (just code), that includes a secondary registry so you can set specific coercers to match the same spec keyword: https://gist.github.com/wilkerlucio/08fb5f858a12b86f63c76cf453cd90c0
This way you don't enforce the coercion, making your validations faster and giving you more control over when to coerce (which usually should only happen at the borders of your system).