Basic coercion is explained in detail in the Coercion Guide. With Ring, both request parameters and response bodies can be coerced.
The following request parameters are currently supported:
type | request source |
---|---|
:query |
:query-params |
:body |
:body-params |
:request |
:body-params , allows per-content-type coercion |
:form |
:form-params |
:header |
:header-params |
:path |
:path-params |
:multipart |
:multipart-params , see Default Middleware |
To enable coercion, the following things need to be done:
- Define a
reitit.coercion/Coercion
for the routes - Define types for the parameters and/or responses
- Mount Coercion Middleware to apply to coercion
- Use the coerced parameters in a handler/middleware
reitit.coercion/Coercion
is a protocol defining how types are defined, coerced and inventoried.
Reitit ships with the following coercion modules:
reitit.coercion.malli/coercion
for mallireitit.coercion.schema/coercion
for plumatic schemareitit.coercion.spec/coercion
for both clojure.spec and data-specs
Coercion can be attached to route data under :coercion
key. There can be multiple Coercion
implementations within a single router, normal scoping rules apply.
Parameters are defined in route data under :parameters
key. It's value should be a map of parameter :type
-> Coercion Schema.
Responses are defined in route data under :responses
key. It's value should be a map of http status code to a map which can contain :body
key with Coercion Schema as value.
Below is an example with Plumatic Schema. It defines schemas for :query
, :body
and :path
parameters and for http 200 response :body
.
Handlers can access the coerced parameters via the :parameters
key in the request.
(require '[reitit.coercion.schema])
(require '[schema.core :as s])
(def PositiveInt (s/constrained s/Int pos? 'PositiveInt))
(def plus-endpoint
{:coercion reitit.coercion.schema/coercion
:parameters {:query {:x s/Int}
:body {:y s/Int}
:path {:z s/Int}}
:responses {200 {:body {:total PositiveInt}}}
:handler (fn [{:keys [parameters]}]
(let [total (+ (-> parameters :query :x)
(-> parameters :body :y)
(-> parameters :path :z))]
{:status 200
:body {:total total}}))})
Defining a coercion for a route data doesn't do anything, as it's just data. We have to attach some code to apply the actual coercion. We can use the middleware from reitit.ring.coercion
:
coerce-request-middleware
to apply the parameter coercioncoerce-response-middleware
to apply the response coercioncoerce-exceptions-middleware
to transform coercion exceptions into pretty responses
Here is a full example for applying coercion with Reitit, Ring and Schema:
(require '[reitit.ring.coercion :as rrc])
(require '[reitit.coercion.schema])
(require '[reitit.ring :as ring])
(require '[schema.core :as s])
(def PositiveInt (s/constrained s/Int pos? 'PositiveInt))
(def app
(ring/ring-handler
(ring/router
["/api"
["/ping" {:name ::ping
:get (fn [_]
{:status 200
:body "pong"})}]
["/plus/:z" {:name ::plus
:post {:coercion reitit.coercion.schema/coercion
:parameters {:query {:x s/Int}
:body {:y s/Int}
:path {:z s/Int}}
:responses {200 {:body {:total PositiveInt}}}
:handler (fn [{:keys [parameters]}]
(let [total (+ (-> parameters :query :x)
(-> parameters :body :y)
(-> parameters :path :z))]
{:status 200
:body {:total total}}))}}]]
{:data {:middleware [rrc/coerce-exceptions-middleware
rrc/coerce-request-middleware
rrc/coerce-response-middleware]}})))
Valid request:
(app {:request-method :post
:uri "/api/plus/3"
:query-params {"x" "1"}
:body-params {:y 2}})
; {:status 200, :body {:total 6}}
Invalid request:
(app {:request-method :post
:uri "/api/plus/3"
:query-params {"x" "abba"}
:body-params {:y 2}})
; {:status 400,
; :body {:schema {:x "Int", "Any" "Any"},
; :errors {:x "(not (integer? \"abba\"))"},
; :type :reitit.coercion/request-coercion,
; :coercion :schema,
; :value {:x "abba"},
; :in [:request :query-params]}}
Invalid response:
(app {:request-method :post
:uri "/api/plus/3"
:query-params {"x" "1"}
:body-params {:y -10}})
; {:status 500,
; :body {:schema {:total "(constrained Int PositiveInt)"},
; :errors {:total "(not (PositiveInt -6))"},
; :type :reitit.coercion/response-coercion,
; :coercion :schema,
; :value {:total -6},
; :in [:response :body]}}
You can also specify request and response body schemas per content-type. These are also read by the OpenAPI feature when generating api docs. The syntax for this is:
(def app
(ring/ring-handler
(ring/router
["/api"
["/example" {:post {:coercion reitit.coercion.schema/coercion
:request {:content {"application/json" {:schema {:y s/Int}}
"application/edn" {:schema {:z s/Int}}
;; default if no content-type matches:
:default {:schema {:yy s/Int}}}}
:responses {200 {:content {"application/json" {:schema {:w s/Int}}
"application/edn" {:schema {:x s/Int}}
:default {:schema {:ww s/Int}}}}}
:handler ...}}]]
{:data {:middleware [rrc/coerce-exceptions-middleware
rrc/coerce-request-middleware
rrc/coerce-response-middleware]}})))
Spec problems are exposed as is in request & response coercion errors. Pretty-printers like expound can be enabled like this:
(require '[reitit.ring :as ring])
(require '[reitit.ring.middleware.exception :as exception])
(require '[reitit.ring.coercion :as coercion])
(require '[expound.alpha :as expound])
(defn coercion-error-handler [status]
(let [printer (expound/custom-printer {:theme :figwheel-theme, :print-specs? false})
handler (exception/create-coercion-handler status)]
(fn [exception request]
(printer (-> exception ex-data :problems))
(handler exception request))))
(def app
(ring/ring-handler
(ring/router
["/plus"
{:get
{:parameters {:query {:x int?, :y int?}}
:responses {200 {:body {:total pos-int?}}}
:handler (fn [{{{:keys [x y]} :query} :parameters}]
{:status 200, :body {:total (+ x y)}})}}]
{:data {:coercion reitit.coercion.spec/coercion
:middleware [(exception/create-exception-middleware
(merge
exception/default-handlers
{:reitit.coercion/request-coercion (coercion-error-handler 400)
:reitit.coercion/response-coercion (coercion-error-handler 500)}))
coercion/coerce-request-middleware
coercion/coerce-response-middleware]}})))
(app
{:uri "/plus"
:request-method :get
:query-params {"x" "1", "y" "fail"}})
; => ...
; -- Spec failed --------------------
;
; {:x ..., :y "fail"}
; ^^^^^^
;
; should satisfy
;
; int?
(app
{:uri "/plus"
:request-method :get
:query-params {"x" "1", "y" "-2"}})
; => ...
;-- Spec failed --------------------
;
; {:total -1}
; ^^
;
; should satisfy
;
; pos-int?
The coercion middlewares are compiled against a route. In the middleware compilation step the actual coercer implementations are constructed for the defined models. Also, the middleware doesn't mount itself if a route doesn't have :coercion
and :parameters
or :responses
defined.
We can query the compiled middleware chain for the routes:
(require '[reitit.core :as r])
(-> (ring/get-router app)
(r/match-by-name ::plus)
:result :post :middleware
(->> (mapv :name)))
; [::mw/coerce-exceptions
; ::mw/coerce-request
; ::mw/coerce-response]
Route without coercion defined:
(app {:request-method :get, :uri "/api/ping"})
; {:status 200, :body "pong"}
Has no mounted middleware:
(-> (ring/get-router app)
(r/match-by-name ::ping)
:result :get :middleware
(->> (mapv :name)))
; []