In the last post I set up all the basic config to process Json using json4s in the context of Akka HTTP.

In this example I’ll show how to “Unmarshal” or deserialize Json from wire format to an instance of a Scala case class. More about how Akka HTTP handles JSON via marshalling and unmarshalling

Let’s start by defining a case class that represents the JSON contract for an endpoint:

final case class JsonRequestBody(userName: String, content: String)

Since we set up a custom trait for Json support we have the ability to deserialize or “unmarshall” Json that comes thru in a request body.

I’m new to Akka and I am just getting used to its DSL. But I like it so far. For example in this request, the post directive extracts the request method, then passes the request to its inner route handler if it is valid. If not, it rejects the request. The the inner directive, entity, receives the request and ‘Unmarshalls the requests entity to the given type and passes it to its inner Route’. The whole thing adds a functional feel to HTTP which I dig.

Here’s an example with annotations:

// match on the post request
val postHome = post {
  // Unmarshalls the requests entity and yields to inner block
  entity(as[JsonRequestBody]) { jsonReq => {
    val userName = jsonReq.userName
    val content = jsonReq.content
    complete(
      // here's an example of how to respond with a combination of json fields
      // using json4s Extraction
      Extraction.decompose(
        Map(
          "message" -> s"Welcome, $userName, you said '$content'",
          "sessionId" -> scala.util.Random.nextInt(100000),
          "customObject" -> JsonRequestBody("bob", "helloWorld"),
          "float" -> scala.util.Random.nextFloat,
          "listOfInts" -> List(2, 3, 5, 7, 11),
          "nestedObject" -> Map(
            "nested" -> true,
            "nestedList" -> List(13, 17, 19, 23)
          )
        )
      )
    )
  }}
}

Accessing the endpoint with a POST and a Json blob that matches the JsonRequestBody case class I mentioned at the top of the post.

curl -X POST \
  http://localhost:8080/home \
  -H 'content-type: application/json' \
  -d '{
	"userName": "bob",
	"content": "testing"
}' | jq

# expected JSON is here!

Excellent!

Akka HTTP will also reply with a helpful error if the request body does not match the expected type:

The request content was malformed:
No usable value for content
Did not find value which can be converted into java.lang.String

Here’s the full version:

package com.lombardo.server

import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.server.{Directives, PathMatchers, Route}
import akka.stream.ActorMaterializer
import de.heikoseeberger.akkahttpjson4s.Json4sSupport
import org.json4s.Extraction
import org.json4s.JsonDSL._
import org.json4s.jackson.JsonMethods.render

import scala.concurrent.ExecutionContextExecutor
import scala.util.{Failure, Success}

trait JsonSupport extends Json4sSupport {
  implicit val serialization = org.json4s.native.Serialization
  implicit val json4sFormats = org.json4s.DefaultFormats
}

final case class JsonRequestBody(userName: String, content: String)

class WebServer(implicit system: ActorSystem, materializer: ActorMaterializer,
                executionContext: ExecutionContextExecutor)
  extends Directives with JsonSupport {

  val getHome = get {
    complete(render("message" -> "welcome to the fun house"))
  }

  val postHome = post {
    entity(as[JsonRequestBody]) { jsonReq => {
      val userName = jsonReq.userName
      val content = jsonReq.content
      complete(
        Extraction.decompose(
          Map(
            "message" -> s"Welcome, $userName, you said '$content'",
            "sessionId" -> scala.util.Random.nextInt(100000),
            "customObject" -> JsonRequestBody("bob", "helloWorld"),
            "float" -> scala.util.Random.nextFloat,
            "listOfInts" -> List(2, 3, 5, 7, 11),
            "nestedObject" -> Map(
              "nested" -> true,
              "nestedList" -> List(13, 17, 19, 23)
            )
          )
        )
      )
    }}
  }

  val restfulRoutes: Route = path("home") {
    getHome ~ postHome
  }

  val bindingFuture = Http().bindAndHandle(restfulRoutes, "0.0.0.0", 8080)

  bindingFuture.onComplete {
    case Success(binding) 
      println(s"Webserver is listening on localhost:8080")
    case Failure(e) 
      println(s"Binding failed with ${e.getMessage}")
      system.terminate()
  }
}