Combine Query String Parameters with JSON Entity in Spray 1.2.0 Routing

1.1k views Asked by At

Using Spray Routing, I would like have a single directive that merges the query string parameters with a JSON entity, with both being optional. I would want to have this happen before any marshalling happens.

Something like this:

val myRoute = mergedParametersAndEntity(as[DomainSpecificClass]) { myobj =>
  // ... code code code ...
  complete(OK, myobj.someMethod)
}

Basically what I was hoping for was the following behavior:

When someone does a request like:

POST /v1/resource?a=helloQS&b=helloQS
Content-Type: application/json

{"a":"helloFromJson","c":"helloFromJson"}

Then the object above (myobj) could contain the keys:

a -> helloFromJson
b -> helloQS
c -> helloFromJson

In other words, items specified in the request body would override things in the query string. I know this must be possible somehow, but I simply cannot figure out how to do it. Can anyone help?

Thank you!

2

There are 2 answers

0
eugenijusr On

If anyone is still wondering how to do this here's a small Spray directive that allows copying param values to the JSON before unmarshalling it. It uses JSON lenses (https://github.com/jrudolph/json-lenses) to modify the request body before unmarshalling.

def updateJson(update: Update): Directive0 = {
  mapRequest((request: HttpRequest) => {
    request.entity match {
      case Empty => request
      case NonEmpty(contentType, data) =>
        try {
          request.copy(entity = HttpEntity(`application/json`, JsonParser(data.asString).update(update).toString))
        }
        catch {
          case e: ParsingException => request
        }
    }
  })
}

And here's how you use it. Say you want to update a resource with a PUT request and pass the ID from the URL to the unmarshaller (a very common scenario btw):

// Model
case class Thing(id: Long, title: String, description: String, createdAt: DateTime)

// Actor message for the update operation
case class UpdateThing(id: Long, title: String)

// Spray routing
def thingsRoute = 
  path("things" / Segment) { id =>
    put {
      updateJson(field("id") ! set[Long](id)) {
        entity(as[UpdateThing]) { updateThing =>
          complete {
            // updateThing now has the ID set
            (someActor ? updateThing).mapTo[Thing]
          }
        }
      }
    }
  }

You can also combine it with the parameters directive to set arbitrary GET params.

0
Tim Harper On

My suggestion: don't take this approach. It feels dirty. Go back to the drawing board if you can.

With that in mind, you won't be able to interject a merge like you want with the existing JSON marshalling support in Spray. You'll need to stitch it together yourself. Something like this should at least point you in the right direction (provided it be the direction you must go):

import org.json4s.JsonAST._
import org.json4s.JsonDSL._
def mergedParametersAndEntity[T](implicit m:Manifest[T]): Directive1[T] = {
  entity(as[JObject]).flatMap { jObject =>
    parameterMap flatMap { params =>
      val left = JObject(params.mapValues { v => JString(v) }.toSeq : _*)
      provide(left.merge(jObject).extract[T])
    }
  }
}