Repository: oliyh/martian
Pull request #110 (2021-04-01 15:10:50)

Specific aliases and improving defaults


Patch:
diff --git a/README.md b/README.md
index 041f97d..d57c4b7 100644
--- a/README.md
+++ b/README.md
@@ -359,10 +359,10 @@ non-strict mode will allow it. Strict mode defaults to false.
 
 ## Defaults
 
-Martian can read `default` directives from Swagger, or you can supply them if bootstrapping from data. They can be seen using `explore` and merged with your params if you include the optional `merge-defaults` interceptor.
+Martian can read `default` directives from Swagger, or you can supply them if bootstrapping from data. They can be seen using `explore` and merged with your params if you set the optional `use-defaults?` option.
 
 ```clojure
-(require '[martian.schema :refer [schema-with-meta]])
+(require '[schema-tools.core :as st])
 (require '[martian.interceptors :refer [merge-defaults]])
 
 (let [m (martian/bootstrap "https://api.org"
@@ -370,11 +370,11 @@ Martian can read `default` directives from Swagger, or you can supply them if bo
                              :path-parts ["/pets/"]
                              :method :post
                              :body-schema {:pet {:id   s/Int
-                                                 :name (schema-with-meta s/Str {:default "Bryson"})}}}]
-                           {:interceptors (cons (merge-defaults) martian/default-interceptors)})]
+                                                 :name (st/default s/Str "Bryson")}}}]
+                           {:use-defaults? true})]
 
   (martian/explore m :create-pet)
-  ;; {:summary nil, :parameters {:pet {:id Int, :name (schema-with-meta Str {:default "Bryson"})}}, :returns {}}
+  ;; {:summary nil, :parameters {:pet {:id Int, :name (default Str "Bryson")}}, :returns {}}
 
   (martian/request-for m :create-pet {:pet {:id 123}})
   ;; {:method :post, :url "https://api.org/pets/", :body {:id 123, :name "Bryson"}}
diff --git a/core/project.clj b/core/project.clj
index 21c61eb..2c48f80 100644
--- a/core/project.clj
+++ b/core/project.clj
@@ -5,6 +5,7 @@
             :url "http://opensource.org/licenses/MIT"}
   :dependencies [[frankiesardo/tripod "0.2.0"]
                  [prismatic/schema "1.1.9"]
+                 [metosin/schema-tools "0.12.3"]
                  [metosin/spec-tools "0.7.0"
                   :exclusions [com.fasterxml.jackson.core/jackson-databind]]
                  [org.clojure/spec.alpha "0.2.176"]
diff --git a/core/src/martian/core.cljc b/core/src/martian/core.cljc
index d071fb5..c98ee31 100644
--- a/core/src/martian/core.cljc
+++ b/core/src/martian/core.cljc
@@ -1,13 +1,11 @@
 (ns martian.core
   (:require [tripod.context :as tc]
-            [camel-snake-kebab.core :refer [->kebab-case-keyword]]
             [clojure.string :as string]
-            [clojure.walk :refer [keywordize-keys postwalk-replace]]
+            [clojure.walk :refer [keywordize-keys]]
             [martian.interceptors :as interceptors]
-            [martian.schema :as schema]
+            [martian.parameter-aliases :as parameter-aliases :refer [parameter-aliases alias-schema]]
             [martian.swagger :refer [swagger->handlers]]
             [martian.openapi :refer [openapi->handlers openapi-schema?]]
-            [schema.core :as s]
             [clojure.spec.alpha :as spec]
             [martian.spec :as mspec]))
 
@@ -25,10 +23,11 @@
 
 (defn- enrich-handler [handler]
   (-> handler
-      (assoc :parameter-aliases (let [ks (->> (map handler parameter-schemas)
-                                              schema/parameter-keys
-                                              (remove #(= % s/Any)))]
-                                  (zipmap (map ->kebab-case-keyword ks) ks)))))
+      (assoc :parameter-aliases
+             (reduce (fn [aliases parameter-key]
+                       (assoc aliases parameter-key (parameter-aliases (get handler parameter-key))))
+                     {}
+                     parameter-schemas))))
 
 (defn- concise->handlers [concise-handlers global-produces global-consumes]
   (map (comp
@@ -64,14 +63,14 @@
                                handler))
                            %)))
 
-(defrecord Martian [api-root handlers interceptors])
+(defrecord Martian [api-root handlers interceptors opts])
 
 (defn url-for
   ([martian route-name] (url-for martian route-name {}))
   ([martian route-name params]
    (let [{:keys [api-root handlers]} (resolve-instance martian)]
      (when-let [handler (find-handler handlers route-name)]
-       (let [params (->> params keywordize-keys (interceptors/coerce-data handler :path-schema))]
+       (let [params (interceptors/coerce-data handler :path-schema (keywordize-keys params) (:opts martian))]
          (str api-root (string/join (map #(get params % %) (:path-parts handler)))))))))
 
 (defn request-for
@@ -85,7 +84,8 @@
                            :url-for (partial url-for martian)
                            :request (or (::request params) {})
                            :handler handler
-                           :params params))))))))
+                           :params params
+                           :opts (:opts martian)))))))))
 
 (defn response-for
   ([martian route-name] (response-for martian route-name {}))
@@ -98,23 +98,24 @@
                             :url-for (partial url-for martian)
                             :request (or (::request params) {})
                             :handler handler
-                            :params params))))))))
+                            :params params
+                            :opts (:opts martian)))))))))
 
 (defn explore
   ([martian] (mapv (juxt :route-name :summary) (:handlers (resolve-instance martian))))
   ([martian route-name]
-   (when-let [handler (find-handler (:handlers (resolve-instance martian)) route-name)]
-     {:summary (:summary handler)
-      :parameters (let [parameter-aliases (:parameter-aliases handler)]
-                    (->> (map handler parameter-schemas)
-                         (apply merge)
-                         (postwalk-replace (zipmap (vals parameter-aliases) (keys parameter-aliases)))))
+   (when-let [{:keys [parameter-aliases summary] :as handler} (find-handler (:handlers (resolve-instance martian)) route-name)]
+     {:summary summary
+      :parameters (reduce (fn [params parameter-key]
+                            (merge params (alias-schema (get parameter-aliases parameter-key) (get handler parameter-key))))
+                          {}
+                          parameter-schemas)
       :returns (->> (:response-schemas handler)
                     (map (juxt (comp :v :status) :body))
                     (into {}))})))
 
-(defn- build-instance [api-root handlers {:keys [interceptors]}]
-  (->Martian api-root handlers (or interceptors default-interceptors)))
+(defn- build-instance [api-root handlers {:keys [interceptors] :as opts}]
+  (->Martian api-root handlers (or interceptors default-interceptors) (dissoc opts :interceptors)))
 
 (spec/fdef build-instance
   :args (spec/cat :api-root ::mspec/api-root
diff --git a/core/src/martian/defaults.cljc b/core/src/martian/defaults.cljc
deleted file mode 100644
index 8d8bb50..0000000
--- a/core/src/martian/defaults.cljc
+++ /dev/null
@@ -1,30 +0,0 @@
-(ns martian.defaults
-  (:require [schema.core :as s #?@(:cljs [:refer [MapEntry EqSchema]])]
-            [schema.spec.core :as spec]
-            [martian.schema :refer [#?(:cljs SchemaWithMeta)]])
-  #?(:clj (:import [schema.core MapEntry EqSchema]
-                   [martian.schema SchemaWithMeta])))
-
-(defn get-default [schema]
-  (when (instance? SchemaWithMeta schema) (get-in schema [:meta :default])))
-
-(defn- with-paths [path schema]
-  (map (fn [schema]
-         {:path (if (and (instance? MapEntry schema)
-                         (instance? EqSchema (:key-schema schema)))
-                  (conj path (:v (:key-schema schema)))
-                  path)
-          :schema schema})
-       (spec/subschemas (s/spec schema))))
-
-(defn defaults [schema]
-  (when schema
-    (loop [defaults {}
-           paths-and-schemas (with-paths [] schema)]
-      (if-let [{:keys [path schema]} (first paths-and-schemas)]
-        (if-let [default (get-default schema)]
-          (recur (assoc-in defaults path default) (concat (rest paths-and-schemas)
-                                                          (with-paths path schema)))
-          (recur defaults (concat (rest paths-and-schemas)
-                                  (with-paths path schema))))
-        defaults))))
diff --git a/core/src/martian/interceptors.cljc b/core/src/martian/interceptors.cljc
index 75537d5..68ac26d 100644
--- a/core/src/martian/interceptors.cljc
+++ b/core/src/martian/interceptors.cljc
@@ -1,6 +1,5 @@
 (ns martian.interceptors
   (:require [martian.schema :as schema]
-            [martian.defaults :refer [defaults]]
             [clojure.walk :refer [keywordize-keys stringify-keys]]
             [tripod.context :as tc]
             [schema.core :as s]
@@ -36,8 +35,8 @@
    :enter (fn [{:keys [params url-for handler] :as ctx}]
             (update ctx :request create-only :url (url-for (:route-name handler) params)))})
 
-(defn coerce-data [{:keys [parameter-aliases] :as handler} schema-key params]
-  (schema/coerce-data (get handler schema-key) params parameter-aliases))
+(defn coerce-data [{:keys [parameter-aliases] :as handler} schema-key params opts]
+  (schema/coerce-data (get handler schema-key) params (get parameter-aliases schema-key) (:use-defaults? opts)))
 
 (def keywordize-params
   {:name ::keywordize-params
@@ -45,30 +44,30 @@
 
 (def set-query-params
   {:name ::query-params
-   :enter (fn [{:keys [params handler] :as ctx}]
-            (update ctx :request insert-or-merge :query-params (coerce-data handler :query-schema params)))})
+   :enter (fn [{:keys [params handler opts] :as ctx}]
+            (update ctx :request insert-or-merge :query-params (coerce-data handler :query-schema params opts)))})
 
 (def set-body-params
   {:name ::body-params
-   :enter (fn [{:keys [params handler] :as ctx}]
-            (if-let [[body-key body-schema] (first (:body-schema handler))]
-              (let [parameter-aliases (:parameter-aliases handler)
+   :enter (fn [{:keys [params handler opts] :as ctx}]
+            (if-let [[body-key] (first (:body-schema handler))]
+              (let [body-key (s/explicit-schema-key body-key)
                     body-params (or (:martian.core/body params)
-                                    (get params (s/explicit-schema-key body-key))
-                                    (get params (->kebab-case-keyword (s/explicit-schema-key body-key)))
+                                    (get params body-key)
+                                    (get params (->kebab-case-keyword body-key))
                                     params)]
-                (update ctx :request insert-or-merge :body (schema/coerce-data body-schema body-params parameter-aliases)))
+                (update ctx :request insert-or-merge :body (get (coerce-data handler :body-schema {body-key body-params} opts) body-key)))
               ctx))})
 
 (def set-form-params
   {:name ::form-params
-   :enter (fn [{:keys [params handler] :as ctx}]
-            (update ctx :request insert-or-merge :form-params (coerce-data handler :form-schema params)))})
+   :enter (fn [{:keys [params handler opts] :as ctx}]
+            (update ctx :request insert-or-merge :form-params (coerce-data handler :form-schema params opts)))})
 
 (def set-header-params
   {:name ::header-params
-   :enter (fn [{:keys [params handler] :as ctx}]
-            (update ctx :request insert-or-merge :headers (stringify-keys (coerce-data handler :headers-schema params))))})
+   :enter (fn [{:keys [params handler opts] :as ctx}]
+            (update ctx :request insert-or-merge :headers (stringify-keys (coerce-data handler :headers-schema params opts))))})
 
 (def enqueue-route-specific-interceptors
   {:name ::enqueue-route-specific-interceptors
@@ -129,26 +128,6 @@
                                   :response-schemas (:response-schemas handler)}))))
              ctx)}))
 
-(defn- deep-merge [a b]
-  (merge-with
-   (fn [a b]
-     (if (every? map? [a b])
-       (deep-merge a b)
-       b))
-   a b))
-
-(defn merge-defaults
-  "Read default values from the specified (default: all) input schemas and merge into the params map"
-  ([] (merge-defaults [:path-schema :query-schema :body-schema :form-schema :headers-schema]))
-  ([schema-keys]
-   {:name ::merge-defaults
-    :enter (fn [{:keys [handler] :as ctx}]
-             (update ctx :params (fn [params]
-                                   (reduce (fn [p schema-key]
-                                             (deep-merge (defaults (get handler schema-key)) p))
-                                           params
-                                           schema-keys))))}))
-
 (defn supported-content-types
   "Return the full set of supported content-types as declared by any encoding/decoding interceptors"
   [interceptors]
diff --git a/core/src/martian/parameter_aliases.cljc b/core/src/martian/parameter_aliases.cljc
new file mode 100644
index 0000000..31525b4
--- /dev/null
+++ b/core/src/martian/parameter_aliases.cljc
@@ -0,0 +1,54 @@
+(ns martian.parameter-aliases
+  (:require [schema.core :as s]
+            [camel-snake-kebab.core :refer [->kebab-case]]
+            [clojure.set :refer [rename-keys]]
+            [martian.schema-tools :refer [key-seqs prewalk-with-path]]))
+
+;; todo lean on schema-tools.core for some of this
+
+(defn ->idiomatic [k]
+  (when (and k (s/specific-key? k) (not (and (keyword? k) (namespace k))))
+    (->kebab-case (s/explicit-schema-key k))))
+
+(defn- idiomatic-path [path]
+  (vec (keep ->idiomatic path)))
+
+(defn parameter-aliases
+  "Produces a data structure for use with `unalias-data`"
+  [schema]
+  (reduce (fn [acc path]
+            (if-let [idiomatic-key (some-> path last ->idiomatic)]
+              (if-not (= (last path) idiomatic-key)
+                (update acc (idiomatic-path (drop-last path)) merge {idiomatic-key (last path)})
+                acc)
+              acc))
+          {}
+          (key-seqs schema)))
+
+(defn unalias-data
+  "Takes parameter aliases and (deeply nested) data, returning data with deeply-nested keys renamed as described by parameter-aliases"
+  [parameter-aliases x]
+  (if parameter-aliases
+    (prewalk-with-path (fn [path x]
+                         (if (map? x)
+                           (rename-keys x (get parameter-aliases (idiomatic-path path)))
+                           x))
+                       []
+                       x)
+    x))
+
+(defn alias-schema
+  "Walks a schema, transforming all keys into their aliases (idiomatic keys)"
+  [parameter-aliases schema]
+  (if parameter-aliases
+    (prewalk-with-path (fn [path x]
+                         (if (map? x)
+                           (let [kmap (reduce-kv (fn [kmap k v]
+                                                   (assoc kmap v k (s/optional-key v) (s/optional-key k)))
+                                                 {}
+                                                 (get parameter-aliases (idiomatic-path path)))]
+                             (rename-keys x kmap))
+                           x))
+                       []
+                       schema)
+    schema))
diff --git a/core/src/martian/schema.cljc b/core/src/martian/schema.cljc
index 677eebb..ae1bc03 100644
--- a/core/src/martian/schema.cljc
+++ b/core/src/martian/schema.cljc
@@ -3,17 +3,11 @@
                :cljs [schema.core :as s :refer [AnythingSchema Maybe EnumSchema EqSchema]])
             #?(:cljs [goog.Uri])
             [schema.coerce :as sc]
-            [clojure.walk :refer [postwalk-replace]])
+            [schema-tools.core :as st]
+            [schema-tools.coerce :as stc]
+            [martian.parameter-aliases :refer [unalias-data]])
   #?(:clj (:import [schema.core AnythingSchema Maybe EnumSchema EqSchema])))
 
-(defrecord SchemaWithMeta [schema meta]
-  s/Schema
-  (spec [this] (s/spec schema))
-  (explain [this] (list 'schema-with-meta (s/explain schema) meta)))
-
-(defn schema-with-meta [schema meta]
-  (->SchemaWithMeta schema meta))
-
 (defn- keyword->string [s]
   (if (keyword? s) (name s) s))
 
@@ -24,73 +18,45 @@
                  (string? (.-v ^EqSchema schema))))
     keyword->string))
 
-(declare coercion-matchers)
-
-(defn- schema-with-meta-matcher [schema]
-  (when (instance? SchemaWithMeta schema)
-    (coercion-matchers (:schema schema))))
-
 (defn coercion-matchers [schema]
   (or (sc/string-coercion-matcher schema)
       ({s/Str keyword->string} schema)
-      (string-enum-matcher schema)
-      (schema-with-meta-matcher schema)))
+      (string-enum-matcher schema)))
+
+(defn build-coercion-matchers [use-defaults?]
+  (if use-defaults?
+    (fn [schema]
+      (or (stc/default-matcher schema)
+          (coercion-matchers schema)))
+    coercion-matchers))
 
 (defn- from-maybe [s]
   (if (instance? Maybe s)
     (:schema s)
     s))
 
-(defn- unalias-parameters [data parameter-aliases]
-  (postwalk-replace parameter-aliases data))
-
 (defn coerce-data
   "Extracts the data referred to by the schema's keys and coerces it"
-  [schema data & [parameter-aliases]]
-  (when-let [s (from-maybe schema)]
-    (cond
-      (or (coercion-matchers schema)
-          (instance? AnythingSchema s))
-      ((sc/coercer! schema coercion-matchers) data)
-
-      (map? s)
-      (if (every? s/specific-key? (keys s))
-        (some->> (keys s)
-                 (map s/explicit-schema-key)
-                 (select-keys (unalias-parameters data parameter-aliases))
-                 ((sc/coercer! schema coercion-matchers)))
-        (some->> (unalias-parameters data parameter-aliases)
-                 ((sc/coercer! schema coercion-matchers))))
-
-      (coll? s) ;; primitives, arrays, arrays of maps
-      ((sc/coercer! schema coercion-matchers)
-       (map #(if (map? %)
-               (unalias-parameters % parameter-aliases)
-               %)
-            data))
-
-      :else
-      ((sc/coercer! schema coercion-matchers) data))))
-
-(defn- extract-keys-from-map-schema [schema]
-  (->> (keys schema)
-       (filter s/specific-key?)
-       (map s/explicit-schema-key)))
-
-(defn parameter-keys [schemas]
-  (mapcat
-   (fn [schema]
-     (when-let [s (from-maybe schema)]
-       (cond
-         (and (map? s) (not (record? s)))
-         (concat (extract-keys-from-map-schema s) (parameter-keys (vals s)))
-
-         (coll? s)
-         (parameter-keys s)
-
-         :else
-         nil)))
-   schemas))
+  [schema data & [parameter-aliases use-defaults?]]
+  (let [coercion-matchers (build-coercion-matchers use-defaults?)]
+    (when-let [s (from-maybe schema)]
+      (cond
+        (or (coercion-matchers schema)
+            (instance? AnythingSchema s))
+        ((sc/coercer! schema coercion-matchers) data)
+
+        (map? s)
+        (stc/coerce (unalias-data parameter-aliases data) s (stc/forwarding-matcher coercion-matchers stc/map-filter-matcher))
+
+        (coll? s) ;; primitives, arrays, arrays of maps
+        ((sc/coercer! schema coercion-matchers)
+         (map #(if (map? %)
+                 (unalias-data parameter-aliases %)
+                 %)
+              data))
+
+        :else
+        ((sc/coercer! schema coercion-matchers) data)))))
 
 (declare make-schema)
 
@@ -128,7 +94,7 @@
 
 (defn wrap-default [{:keys [default]} schema]
   (if (some? default)
-    (schema-with-meta schema {:default default})
+    (st/default schema default)
     schema))
 
 (defn- schema-type [ref-lookup {:keys [type $ref] :as param}]
diff --git a/core/src/martian/schema_tools.cljc b/core/src/martian/schema_tools.cljc
new file mode 100644
index 0000000..a0923d3
--- /dev/null
+++ b/core/src/martian/schema_tools.cljc
@@ -0,0 +1,50 @@
+(ns martian.schema-tools
+  (:require [schema.core :as s #?@(:cljs [:refer [MapEntry EqSchema]])]
+            [schema.spec.core :as spec])
+  #?(:clj (:import [schema.core MapEntry EqSchema])))
+
+;; todo
+;; write some tests and lean on schema-tools.core where possible
+
+(defn with-paths [path schema]
+  (keep (fn [schema]
+          (cond (and (instance? MapEntry schema)
+                     (instance? EqSchema (:key-schema schema)))
+                {:path (conj path (:v (:key-schema schema)))
+                 :schema (:val-schema schema)}
+                (map? schema)
+                {:path path
+                 :schema schema}))
+        (spec/subschemas (s/spec schema))))
+
+(defn key-seqs
+  "Returns a collection of paths which would address all possible entries (using `get-in`) in data described by the schema"
+  [schema]
+  (when (map? schema)
+    (loop [paths [[]]
+           paths-and-schemas (with-paths [] schema)]
+      (if-let [{:keys [path schema]} (first paths-and-schemas)]
+        (recur (conj paths path) (concat (rest paths-and-schemas)
+                                         (with-paths path schema)))
+        paths))))
+
+(defn walk-with-path
+  "Identical to `clojure.walk/walk` except keeps track of the path through the data structure (as per `get-in`)
+   as it goes, calling `inner` and `outer` with two args: the path and form"
+  ([inner outer form] (walk-with-path inner outer [] form))
+  ([inner outer path form]
+   (cond
+     (list? form) (outer path (apply list (map (partial inner path) form)))
+     (map-entry? form)
+     (outer path #?(:clj (clojure.lang.MapEntry/create (inner path (key form)) (inner (conj path (key form)) (val form)))
+                    :cljs (cljs.core/MapEntry. (inner path (key form)) (inner (conj path (key form)) (val form)) nil)))
+     (seq? form) (outer path (doall (map (partial inner path) form)))
+     (record? form) (outer path (reduce (fn [r x] (conj r (inner path x))) form form))
+     (coll? form) (outer path (into (empty form) (map (partial inner path) form)))
+     :else (outer path form))))
+
+(defn postwalk-with-path [f path form]
+  (walk-with-path (partial postwalk-with-path f) f path form))
+
+(defn prewalk-with-path [f path form]
+  (walk-with-path (partial prewalk-with-path f) (fn [_path form] form) path (f path form)))
diff --git a/core/src/martian/spec.cljc b/core/src/martian/spec.cljc
index b5676d1..e59dde4 100644
--- a/core/src/martian/spec.cljc
+++ b/core/src/martian/spec.cljc
@@ -1,7 +1,6 @@
 (ns martian.spec
   (:require [clojure.spec.alpha :as s]
-            [schema.core :as schema]
-            [clojure.spec.alpha :as spec]))
+            [schema.core :as schema]))
 
 (s/def ::api-root string?)
 
@@ -43,7 +42,8 @@
             ::swagger-definition
             ::path]))
 
-(s/def ::interceptor (spec/keys :opt-un [::name ::enter ::leave ::catch]))
-(s/def ::interceptors (spec/nilable (spec/coll-of ::interceptor)))
+(s/def ::interceptor (s/keys :opt-un [::name ::enter ::leave ::catch]))
+(s/def ::interceptors (s/nilable (s/coll-of ::interceptor)))
+(s/def ::use-defaults? boolean?)
 
-(s/def ::opts (spec/nilable (spec/keys :opt-un [::interceptors])))
+(s/def ::opts (s/nilable (s/keys :opt-un [::interceptors ::use-defaults?])))


Test patch:
diff --git a/core/test/martian/core_test.cljc b/core/test/martian/core_test.cljc
index 9c0b2dc..42997db 100644
--- a/core/test/martian/core_test.cljc
+++ b/core/test/martian/core_test.cljc
@@ -9,6 +9,8 @@
 #?(:cljs
    (def Throwable js/Error))
 
+(def cannot-coerce-pattern #"Could not coerce value to schema")
+
 (stest/instrument)
 
 (def swagger-definition
@@ -102,7 +104,7 @@
                                 :consumes ["application/json"]})]
 
       (is (= "https://api.org/pets/123" (martian/url-for m :load-pet {:id 123})))
-      (is (thrown-with-msg? Throwable #"Value cannot be coerced to match schema"
+      (is (thrown-with-msg? Throwable cannot-coerce-pattern
                             (martian/request-for m :load-pet {:id "one"})))
 
       (is (= {:method :post
@@ -269,17 +271,17 @@
                                        :name "nigel"}))))
 
     (testing "exceptions"
-      (is (thrown-with-msg? Throwable #"Value cannot be coerced to match schema"
+      (is (thrown-with-msg? Throwable cannot-coerce-pattern
                             (request-for :all-pets {:sort "baa"})))
 
-      (is (thrown-with-msg? Throwable #"Value cannot be coerced to match schema"
+      (is (thrown-with-msg? Throwable cannot-coerce-pattern
                             (request-for :load-pet {:id "one"})))
 
-      (is (thrown-with-msg? Throwable #"Value cannot be coerced to match schema"
+      (is (thrown-with-msg? Throwable cannot-coerce-pattern
                             (request-for :create-pet {:pet {:id "one"
                                                             :name 1}})))
 
-      (is (thrown-with-msg? Throwable #"Value cannot be coerced to match schema: \{:id missing-required-key, :name missing-required-key\}"
+      (is (thrown-with-msg? Throwable cannot-coerce-pattern
                             (request-for :create-pet))))))
 
 (deftest with-interceptors-test
@@ -318,19 +320,34 @@
                                  :query-schema {:camelVersion s/Int}
                                  :body-schema {:Camel {:camelName s/Str
                                                        :camelTrain {:leaderName s/Str
-                                                                    (s/optional-key :followerCamels) [{:followerName s/Str}]}}}
+                                                                    (s/optional-key :followerCamels) [{:followerName s/Str}]}
+                                                       :anyCamel s/Any}}
                                  :headers-schema {(s/optional-key :camelToken) s/Str}
                                  :form-schema {:camelHumps (s/maybe s/Int)}}])]
 
     (is (= "https://camels.org/camels/1"
            (martian/url-for m :create-camel {:camel-id 1})))
 
+    (is (= {:path-schema {[] {:camel-id :camelId}},
+            :query-schema {[] {:camel-version :camelVersion}},
+            :body-schema {[] {:camel :Camel},
+                          [:camel] {:camel-name :camelName,
+                                    :camel-train :camelTrain,
+                                    :any-camel :anyCamel},
+                          [:camel :camel-train] {:leader-name :leaderName,
+                                                 :follower-camels :followerCamels},
+                          [:camel :camel-train :follower-camels] {:follower-name :followerName}},
+            :form-schema {[] {:camel-humps :camelHumps}},
+            :headers-schema {[] {:camel-token :camelToken}}}
+           (:parameter-aliases (martian/handler-for m :create-camel))))
+
     (is (= {:method :put,
             :url "https://camels.org/camels/1",
             :query-params {:camelVersion 2},
             :body {:camelName "kebab"
                    :camelTrain {:leaderName "camel leader"
-                                :followerCamels [{:followerName "OCaml"}]}},
+                                :followerCamels [{:followerName "OCaml"}]}
+                   :anyCamel {:camel-train "choo choo"}},
             :form-params {:camelHumps 2},
             :headers {"camelToken" "cAmEl"}}
 
@@ -341,7 +358,8 @@
                                                  :camel-humps 2
                                                  :camel-name "kebab"
                                                  :camel-train {:leader-name "camel leader"
-                                                               :follower-camels [{:follower-name "OCaml"}]}})
+                                                               :follower-camels [{:follower-name "OCaml"}]}
+                                                 :any-camel {:camel-train "choo choo"}})
 
            ;; nested under (kebabbed) body key
            (martian/request-for m :create-camel {:camel-id 1
@@ -350,7 +368,8 @@
                                                  :camel-humps 2
                                                  :camel {:camel-name "kebab"
                                                          :camel-train {:leader-name "camel leader"
-                                                                       :follower-camels [{:follower-name "OCaml"}]}}})
+                                                                       :follower-camels [{:follower-name "OCaml"}]}
+                                                         :any-camel {:camel-train "choo choo"}}})
 
            ;; destructured, already in camel case
            (martian/request-for m :create-camel {:camelId 1
@@ -359,7 +378,8 @@
                                                  :camelHumps 2
                                                  :camelName "kebab"
                                                  :camelTrain {:leaderName "camel leader"
-                                                              :followerCamels [{:followerName "OCaml"}]}})
+                                                              :followerCamels [{:followerName "OCaml"}]}
+                                                 :anyCamel {:camel-train "choo choo"}})
 
            ;; nested under (already camelled) body key
            (martian/request-for m :create-camel {:camelId 1
@@ -368,7 +388,8 @@
                                                  :camelHumps 2
                                                  :Camel {:camelName "kebab"
                                                          :camelTrain {:leaderName "camel leader"
-                                                                      :followerCamels [{:followerName "OCaml"}]}}})))
+                                                                      :followerCamels [{:followerName "OCaml"}]}
+                                                         :anyCamel {:camel-train "choo choo"}}})))
 
     (testing "explore shows idiomatic kebab keys"
       (is (= {:summary nil,
@@ -377,8 +398,9 @@
                :camel-version                s/Int,
                :camel                        {:camel-name s/Str
                                               :camel-train {:leader-name s/Str
-                                                            (s/optional-key :follower-camels) [{:follower-name s/Str}]}},
-               :camel-humps                  (s/maybe s/Int),
+                                                            (s/optional-key :follower-camels) [{:follower-name s/Str}]}
+                                              :any-camel s/Any},
+               :camel-humps                  (s/maybe s/Int)
                (s/optional-key :camel-token) s/Str},
               :returns {}}
              (martian/explore m :create-camel))))))
diff --git a/core/test/martian/defaults_test.cljc b/core/test/martian/defaults_test.cljc
deleted file mode 100644
index b36f555..0000000
--- a/core/test/martian/defaults_test.cljc
+++ /dev/null
@@ -1,23 +0,0 @@
-(ns martian.defaults-test
-  (:require [martian.defaults :refer [defaults]]
-            [martian.schema :refer [schema-with-meta]]
-            [schema.core :as s]
-            #?(:clj [clojure.test :refer [deftest testing is]]
-               :cljs [cljs.test :refer-macros [deftest testing is]])))
-
-(deftest map-defaults
-  (testing "defaults top level maps"
-    (is (= {:foo "bar"}
-           (defaults {:foo (schema-with-meta s/Str {:default "bar"})}))))
-
-  (testing "defaults nested maps"
-    (is (= {:foo "bar"
-            :baz {:quu 123}}
-           (defaults {:foo (schema-with-meta s/Str {:default "bar"})
-                      :baz {:quu (schema-with-meta s/Int {:default 123})}})))
-
-    (testing "within nested maps"
-      (is (= {:foo "bar"
-              :baz {:quu {:quux "baffle"}}}
-             (defaults {:foo (schema-with-meta s/Str {:default "bar"})
-                        :baz {:quu {:quux (schema-with-meta s/Str {:default "baffle"})}}}))))))
diff --git a/core/test/martian/interceptors_test.cljc b/core/test/martian/interceptors_test.cljc
index af800f8..0ee9331 100644
--- a/core/test/martian/interceptors_test.cljc
+++ b/core/test/martian/interceptors_test.cljc
@@ -1,9 +1,9 @@
 (ns martian.interceptors-test
   (:require [martian.interceptors :as i]
             [martian.encoders :as encoders]
-            [martian.schema :refer [schema-with-meta]]
             [tripod.context :as tc]
             [schema.core :as s]
+            [schema-tools.core :as st]
             #?(:clj [clojure.test :refer [deftest is testing]]
                :cljs [cljs.test :refer-macros [deftest testing is]])
             #?(:clj [martian.test-utils :as tu])))
@@ -202,36 +202,3 @@
       (is (happy? {:status 500
                    :body {:message "That did not go well"}}
                   false)))))
-
-(deftest merge-defaults-test
-  (testing "merges defaults into params"
-    (let [handler {:path-schema {:foo (schema-with-meta s/Str {:default "bar"})}
-                   :query-schema {:baz (schema-with-meta s/Int {:default 123})}
-                   :body-schema {:pet {:type (schema-with-meta s/Int {:default 123})}}}
-          merge-defaults (fn [params]
-                           (let [ctx {:params params
-                                      :handler handler}]
-                             (:params ((:enter (i/merge-defaults)) ctx))))]
-
-      (testing "reads all default values"
-        (is (= {:foo "bar"
-                :baz 123
-                :pet {:type 123}}
-               (merge-defaults {}))))
-
-      (testing "defaults can be overridden"
-        (is (= {:foo "FOO"
-                :baz "BAZ"
-                :pet nil}
-               (merge-defaults {:foo "FOO"
-                                :baz "BAZ"
-                                :pet nil}))))
-
-      (testing "keeps additional keys"
-        (is (= {:foo "bar"
-                :baz 123
-                :quu "QUU"
-                :pet {:type 123
-                      :name "Tigger"}}
-               (merge-defaults {:quu "QUU"
-                                :pet {:name "Tigger"}})))))))
diff --git a/core/test/martian/openapi_test.cljc b/core/test/martian/openapi_test.cljc
index 26ba39d..a3d2fe0 100644
--- a/core/test/martian/openapi_test.cljc
+++ b/core/test/martian/openapi_test.cljc
@@ -1,7 +1,7 @@
 (ns martian.openapi-test
   (:require [martian.test-helpers #?@(:clj [:refer [json-resource]]
                                       :cljs [:refer-macros [json-resource]])]
-            [martian.schema :refer [schema-with-meta]]
+            [schema-tools.core :as st]
             [clojure.test :refer [deftest is testing]]
             [schema.core :as s]
             [martian.openapi :refer [openapi->handlers]]))
@@ -77,7 +77,7 @@
             :method :get,
             :produces ["application/json"],
             :path-schema {:projectId s/Str},
-            :query-schema {(s/optional-key :key) (schema-with-meta s/Str {:default "some-default-key"})},
+            :query-schema {(s/optional-key :key) (st/default s/Str "some-default-key")},
             :form-schema {},
             :path-parts ["/project/" :projectKey],
             :headers-schema {},
diff --git a/core/test/martian/parameter_aliases_test.cljc b/core/test/martian/parameter_aliases_test.cljc
new file mode 100644
index 0000000..ab69854
--- /dev/null
+++ b/core/test/martian/parameter_aliases_test.cljc
@@ -0,0 +1,66 @@
+(ns martian.parameter-aliases-test
+  (:require [martian.parameter-aliases :refer [parameter-aliases unalias-data alias-schema]]
+            [schema-tools.core :as st]
+            [schema.core :as s]
+            #?(:clj [clojure.test :refer [deftest testing is]]
+               :cljs [cljs.test :refer-macros [deftest testing is]])))
+
+(deftest parameter-aliases-test
+  (testing "produces idiomatic aliases for all keys in a schema"
+    (is (= {[] {:foo-bar :fooBar
+                :bar :BAR
+                :baz :Baz}}
+           (parameter-aliases {:fooBar s/Str
+                               (s/optional-key :BAR) s/Str
+                               :Baz s/Str}))))
+
+  (testing "works on nested maps and sequences"
+    (is (= {[] {:foo-bar :fooBar
+                :bar :BAR
+                :baz :Baz}
+            [:baz] {:quu :QUU
+                    :quux :Quux}
+            [:baz :quux] {:fizz :Fizz}}
+           (parameter-aliases {:fooBar s/Str
+                               (s/optional-key :BAR) s/Str
+                               :Baz {:QUU s/Str
+                                     :Quux [{:Fizz s/Str}]}}))))
+
+  (testing "works on nested maps and sequences"
+    (is (= {[] {:foo-bar :fooBar
+                :bar :BAR
+                :baz :Baz}
+            [:baz] {:quu :QUU
+                    :quux :Quux}
+            [:baz :quux] {:fizz :Fizz}}
+           (parameter-aliases {:fooBar s/Str
+                               (s/optional-key :BAR) s/Str
+                               :Baz (st/default {:QUU s/Str
+                                                 :Quux [{:Fizz s/Str}]}
+                                                {:QUU "hi"
+                                                 :Quux []})})))))
+
+(deftest unalias-data-test
+  (testing "renames idiomatic keys back to original"
+    (let [schema {:FOO s/Str
+                  :fooBar s/Str
+                  (s/optional-key :Bar) s/Str}]
+      (is (= {:FOO "a"
+              :fooBar "b"
+              :Bar "c"}
+             (unalias-data (parameter-aliases schema) {:foo "a" :foo-bar "b" :bar "c"})))))
+
+  (testing "works on nested maps and sequences"
+    (let [schema {:FOO {:fooBar s/Str
+                        (s/optional-key :Bar) [{:BAZ s/Str}]}}]
+      (is (= {:FOO {:fooBar "b"
+                    :Bar [{:BAZ "c"}]}}
+             (unalias-data (parameter-aliases schema) {:foo {:foo-bar "b" :bar [{:baz "c"}]}}))))))
+
+(deftest alias-schema-test
+  (testing "renames the keys in the schema to give an idiomatic input schema"
+    (let [schema {:FOO {:fooBar s/Str
+                        (s/optional-key :Bar) [{:BAZ s/Str}]}}]
+      (is (= {:foo {:foo-bar s/Str
+                    (s/optional-key :bar) [{:baz s/Str}]}}
+             (alias-schema (parameter-aliases schema) schema))))))
diff --git a/core/test/martian/schema_test.cljc b/core/test/martian/schema_test.cljc
index b114af5..98432d2 100644
--- a/core/test/martian/schema_test.cljc
+++ b/core/test/martian/schema_test.cljc
@@ -1,6 +1,7 @@
 (ns martian.schema-test
   (:require [martian.schema :as schema]
             [schema.core :as s]
+            [schema-tools.core :as st]
             #?(:clj [clojure.test :refer [deftest testing is]]
                :cljs [cljs.test :refer-macros [deftest testing is]])))
 
@@ -56,8 +57,8 @@
 
 (deftest default-values-test
   (testing "explaining and printing"
-    (is (= '(schema-with-meta Int {:default 123})
-           (s/explain (schema/schema-with-meta s/Int {:default 123})))))
+    (is (= '(default Int 123)
+           (s/explain (st/default s/Int 123)))))
 
   (let [schema (schema/schemas-for-parameters {} [{:name "id"
                                                    :in "path"
@@ -80,10 +81,10 @@
                                                    :default "foo"}])]
 
     (testing "builds the schemas preserving default information"
-      (is (= {:id (schema/schema-with-meta s/Int {:default 123})
-              :name (schema/schema-with-meta s/Str {:default "Gershwin"})
-              :dog? (schema/schema-with-meta s/Bool {:default false})
-              (s/optional-key :unknown) (s/maybe (schema/schema-with-meta s/Any {:default "foo"}))}
+      (is (= {:id (st/default s/Int 123)
+              :name (st/default s/Str "Gershwin")
+              :dog? (st/default s/Bool false)
+              (s/optional-key :unknown) (s/maybe (st/default s/Any "foo"))}
              schema)))
 
     (testing "works on nested params inside the body"
@@ -94,16 +95,73 @@
             definitions {:Pet {:type "object"
                                :properties {:name {:type "string"
                                                    :required true}
+                                            :tags {:type "array"
+                                                   :items {:schema {:$ref "#/definitions/Tag"}}}
                                             :address {:schema {:$ref "#/definitions/Address"}
                                                       :required true}}}
                          :Address {:type "object"
                                    :properties {:city {:type "string"
                                                        :required true
-                                                       :default "trondheim"}}}}
+                                                       :default "trondheim"}}}
+                         :Tag {:type "object"
+                               :properties {:k {:type "string"
+                                                :required true
+                                                :default "ExampleTag"}} }}
             schema (schema/make-schema {:definitions definitions} body-param)]
         (is (= {:name s/Str
-                :address {:city (schema/schema-with-meta s/Str {:default "trondheim"})}}
-               schema))))))
+                (s/optional-key :tags) [s/Any]
+                :address {:city (st/default s/Str "trondheim")}}
+               schema))
+
+        (testing "and coerces data using default values"
+          (testing "does nothing if values are present"
+            (is (= {:name "Brachiosaurus"
+                    :address {:city "stavanger"}}
+                   (schema/coerce-data schema
+                                       {:name "Brachiosaurus"
+                                        :address {:city "stavanger"}}
+                                       nil
+                                       true))))
+
+         (testing "adds missing values when there are defaults"
+           (is (= {:name "Brachiosaurus"
+                    :address {:city "trondheim"}}
+                  (schema/coerce-data schema
+                                      {:name "Brachiosaurus"
+                                       :address {:city nil}}
+                                      nil
+                                       true))))
+
+         (testing "adds missing keys"
+           (is (= {:name "Brachiosaurus"
+                   :address {:city "trondheim"}}
+                  (schema/coerce-data schema
+                                      {:name "Brachiosaurus"
+                                       :address {}}
+                                      nil
+                                      true))))
+
+         (testing "removing extra keys"
+           (is (= {:name "Brachiosaurus"
+                   :address {:city "trondheim"}}
+                  (schema/coerce-data schema
+                                      {:name "Brachiosaurus"
+                                       :extra "key"
+                                       :address {}}
+                                      nil
+                                      true))))
+
+         ;; doesn't work - a limitation of spec-tools?
+         #_(testing "works inside arrays"
+           (is (= {:name "Brachiosaurus"
+                   :address {:city "stavanger"}
+                   :tags [{}]}
+                  (schema/coerce-data schema
+                                      {:name "Brachiosaurus"
+                                       :address {:city "stavanger"}
+                                       :tags [{:k nil}]}
+                                      nil
+                                      true)))))))))
 
 (deftest uuid-test
   (is (= (s/cond-pre s/Str s/Uuid)
@@ -296,32 +354,13 @@
              (schema/coerce-data
               {s/Keyword s/Any}
               {:a-camel {:another-camel {:camels-everywhere 1}}}
-              {:a-camel :aCamel
-               :another-camel :anotherCamel
-               :camels-everywhere :camelsEverywhere})))))
+              {[] {:a-camel :aCamel}
+               [:a-camel] {:another-camel :anotherCamel}
+               [:a-camel :another-camel] {:camels-everywhere :camelsEverywhere}})))))
 
   (testing "keywords to strings"
     (is (= "foo" (schema/coerce-data s/Str :foo)))))
 
-(deftest parameter-keys-test
-  (is (= [:foo]
-         (schema/parameter-keys [{:foo s/Int}])))
-
-  (is (= [:foo]
-         (schema/parameter-keys [{:foo s/Int
-                                  s/Any s/Any}])))
-
-  (is (= [:foo :bar]
-         (schema/parameter-keys [{:foo s/Int}
-                                 {:bar s/Str}])))
-
-  (is (= [:foo :bar :baz :quu :quux :fizz :buzz]
-         (schema/parameter-keys [{:foo s/Int}
-                                 {:bar s/Str}
-                                 {:baz {:quu s/Bool
-                                        :quux s/Num}}
-                                 {:fizz [{:buzz s/Str}]}]))))
-
 (deftest recursive-schema-test
   (let [schema (schema/make-schema {:definitions {:A {:type       "object"
                                                       :properties {:b {:$ref "#/definitions/B"}}}
diff --git a/core/test/martian/test_runner.cljs b/core/test/martian/test_runner.cljs
index 79dc88f..0024c19 100644
--- a/core/test/martian/test_runner.cljs
+++ b/core/test/martian/test_runner.cljs
@@ -1,10 +1,10 @@
 (ns martian.test-runner
   (:require [martian.core-test]
-            [martian.schema-test]
             [martian.interceptors-test]
-            [martian.swagger-test]
             [martian.openapi-test]
-            [martian.defaults-test]
+            [martian.parameter-aliases-test]
+            [martian.schema-test]
+            [martian.swagger-test]
             [figwheel.main.testing :refer [run-tests-async]]))
 
 (defn -main [& args]
diff --git a/re-frame/test/martian/re_frame_test.cljs b/re-frame/test/martian/re_frame_test.cljs
index 0165a3f..194fe53 100644
--- a/re-frame/test/martian/re_frame_test.cljs
+++ b/re-frame/test/martian/re_frame_test.cljs
@@ -83,7 +83,7 @@
        [#{::create-pet-success ::http-failure}]
        (let [[op-id error] (last (:errors @rdb/app-db))]
          (is (= :create-pet op-id))
-         (is (= "Interceptor Exception: Value cannot be coerced to match schema: {:name (not (cljs$core$string? 1)), :type missing-required-key, :age missing-required-key}"
+         (is (= "Interceptor Exception: Could not coerce value to schema: {:Pet {:name (not (cljs$core$string? 1)), :type missing-required-key, :age missing-required-key}}"
                 (.-message error)))))))))
 
 (deftest re-frame-pending-requests-test
diff --git a/test/test/martian/test_test.cljc b/test/test/martian/test_test.cljc
index 11c7b28..88658e2 100644
--- a/test/test/martian/test_test.cljc
+++ b/test/test/martian/test_test.cljc
@@ -33,7 +33,7 @@
   (let [m (-> (martian/bootstrap-swagger "https://api.com" swagger-definition)
               (martian-test/respond-with-generated {:load-pet :random}))]
 
-    (is (thrown-with-msg? Throwable #"Value cannot be coerced to match schema"
+    (is (thrown-with-msg? Throwable #"Could not coerce value to schema"
                           (martian/response-for m :load-pet {:id "abc"})))
 
     (is (nil? (s/check (s/conditional