Search code examples
swiftvaporvapor-fluent

Vapor 3 decoding content: do/catch for multiple post formats?


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?


Solution

  • 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, Futures 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)
        }
    }