I have a controller whose create action I want to accept JSON either like this:
{ "foo": "bar" }
OR like this:
{ "widget": { "foo": "bar" } }
That is, I want to accept either the widget or the widget wrapped in a containing object. Currently, the create action for my controller looks a lot like this:
func createHandler(_ req: Request) throws -> Future<Widget> {
do {
return try req.content.decode(WidgetCreateHolder.self).flatMap(to: Widget.self) {
return createWidget(from: $0.widget)
}
} catch DecodingError.keyNotFound {
return try req.content.decode(WidgetCreateObject.self).flatMap(to: Widget.self) {
return createWidget(from: $0)
}
}
}
where WidgetCreateObject
looks something like:
struct WidgetCreateObject { var foo: String? }
and where WidgetCreateHolder
looks like:
struct WidgetCreateHolder { var widget: WidgetCreateObject }
That is, my create action should try
to create a holder, but if that fails it should catch the error and try just creating the inner object (a WidgetCreateObject
). However, when I deploy this code to Heroku and make a request with just the inner object JSON, I get this in my logs:
[ ERROR ] DecodingError.keyNotFound: Value required for key 'widget'. (ErrorMiddleware.swift:26)
even though I am trying to catch that error!
How can I get my create action to accept two different formats of JSON object?
Figured it out!
The decode
method returns a Future
, such that the actual decoding (and hence the error) occurs later, not during the do/catch. This means there's no way to catch the error with this do catch.
Luckily, Future
s have a series of methods prepended with catch
; the one I'm interested in is catchFlatMap
, which accepts a closure from Error -> Future<Decodable>
. This method 'catches' the errors thrown in the called Future, and passes the error to the closure, using the result in any downstream futures.
So I was able to change my code to:
func createHandler(_ req: Request) throws -> Future<Widget> {
return try req.content.decode(WidgetCreateHolder.self).catchFlatMap({ _ in
return try req.content.decode(WidgetCreateObject.self).map(to: WidgetCreateHolder.self) {
return WidgetCreateHolder(widget: $0)
}
}).flatMap(to: Widget.self) {
return createWidget(from: $0.widget)
}
}