How to read JSON body from Akka HTTP POST request and send the final response as JSON array

Viewed 3830

I am new to akka http. I have created a program to make post request using http as follow -

object MainController {


  def main(args: Array[String]) {
    implicit val system = ActorSystem()
    implicit val materializer = ActorMaterializer()
    implicit val executionContext = system.dispatcher

    val serverSource = Http().bind(interface = "localhost", port = 9000)

    val requestHandler: HttpRequest => HttpResponse = {
      case HttpRequest(GET, Uri.Path("/welcome"), _, _, _) =>
        HttpResponse(entity = HttpEntity(
          ContentTypes.`text/html(UTF-8)`,
          "<html><body>Welcome to API Application</body></html>"))


      case HttpRequest(POST, Uri.Path("/parseData"), _, entity: HttpEntity, _) =>

// Here Need to read request body which is in json format
        println("1 " + new String(entity.getDataBytes()))
        println("2 " + entity.getDataBytes())
// here need to do some calculations and again construct array of json response and send as HttpResponse
        HttpResponse(entity = "PONG!")

      case r: HttpRequest =>
        r.discardEntityBytes() // important to drain incoming HTTP Entity stream
        HttpResponse(404, entity = "Unknown resource!")
    }

    val bindingFuture = Http().bindAndHandleSync(requestHandler, "localhost", 9000)
    println(s"Server online at http://localhost:8080/\nPress RETURN to stop...")
    StdIn.readLine() // let it run until user presses return
    bindingFuture
      .flatMap(_.unbind()) // trigger unbinding from the port
      .onComplete(_ => system.terminate()) // and shutdown when done

  }
}

As i have mentioned in above code inside "Post" request that I need to read the request body data which is array of json and do some calculations and finally send the processed array of json to HTTPResponse. Tried even High level API as well but it is again getting stuck in Marshalling. can anyone please explain or help me in this.

I have tried another approach as follows -

object MainController {

  // needed to run the route
  implicit val system = ActorSystem()
  implicit val materializer = ActorMaterializer()
  implicit val executionContext = system.dispatcher

  final case class hexRecord(hexstring: String)
  final case class DeviceData(hexData: List[hexRecord])
  // formats for unmarshalling and marshalling

  implicit val contentFormat = jsonFormat1(hexRecord)
  implicit val dataFormat = jsonFormat1(DeviceData)

  def main(args: Array[String]) {

    implicit val formats = org.json4s.DefaultFormats

    val requestBody = List.empty[Map[String, Any]]
    val route: Route =
      concat(
        get {
          path("welcome"){
          complete("Welcome to Parsing Application")}
        },
        post {
          path("parseDeviceData") {
            entity(as[DeviceData]) { data => {
              val result = data.hexData.map(row => {
                val parseData = ParserManager(Hex.decodeHex(row.hexstring.replaceAll("\\s", "").toCharArray))
               val jsonString = Serialization.writePretty(parseData)
                jsonString

              }).toArray 

              complete(result)
            }
            }
          }
        }
      )

    val bindingFuture = Http().bindAndHandle(route, "localhost", 9000)
    println(s"Server online at http://localhost:9000/")
    StdIn.readLine() // let it run until user presses return
    bindingFuture
      .flatMap(_.unbind()) // trigger unbinding from the port
      .onComplete(_ => system.terminate()) // and shutdown when done
  }
}

This result is fine here but I am getting escape characters in output -

[
    " {\n  \"totalsize\" : 128,\n  \"devicetypeuno\" : \"2\"} ",
    " {\n  \"totalsize\" : 128,\n  \"devicetypeuno\" : \"2\"} "
]
4 Answers

This is sample program that reads an array of json values and returns a new json back.

import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.server.Directives._
import akka.stream.ActorMaterializer
import akka.util.Timeout
import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport
import io.circe.generic.semiauto
import io.circe.{Decoder, Encoder}

import scala.concurrent.duration._
import scala.io.StdIn

object WebServer3 extends FailFastCirceSupport {

  def main(args: Array[String]) {

    implicit val system = ActorSystem("my-system")
    implicit val materializer = ActorMaterializer()
    implicit val executionContext = system.dispatcher

    implicit val timeout: Timeout = 2.seconds
    case class Zoo(foo: String, bar: String)
    case class DoneZoo(message: String)
    implicit val zooDecoder: Decoder[Zoo] = semiauto.deriveDecoder[Zoo]
    implicit val zooEncoder: Encoder[Zoo] = semiauto.deriveEncoder[Zoo]
    implicit val doneDecoder: Decoder[DoneZoo] = semiauto.deriveDecoder[DoneZoo]
    implicit val doneEnecoder: Encoder[DoneZoo] =
      semiauto.deriveEncoder[DoneZoo]

    val route = path("parseData") {
      entity(as[List[Zoo]]) { zoo =>
        zoo.foreach(println)
        complete(DoneZoo(zoo.foldLeft("") {
          case (agg, el) => agg + el.bar + el.foo + ";"
        }))
      }
    }

    val bindingFuture = Http().bindAndHandle(route, "localhost", 8080)
    println(s"Server online at http://localhost:8080/\nPress RETURN to stop...")
    StdIn.readLine()
    val _ = bindingFuture.flatMap(_.unbind())
  }
}

The json is defined with the help of case classes and circe semi auto derivation. This creates Encoder/Decoder classes for json conversion. You have to convert these types into Akka specific ones used for entity marsharling and this is done implicitly with the help of de.heikoseeberger.akkahttpcirce.FailFastCirceSupport.

After this, you can use akka route dsl to define your http routes. entity(as[List[Zoo]]) will read http body as json and return back List[Zoo]

You can test this app with the help of curl

curl -v -X POST -H 'Content-type: application/json' --data '[{"foo": "Foo", "bar": "Bar"}]' http://localhost:8080/parseData

Note: Unnecessary use of -X or --request, POST is already inferred.
*   Trying ::1...
* TCP_NODELAY set
* Connection failed
* connect to ::1 port 8080 failed: Connection refused
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8080 (#0)
> POST /parseData HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.64.1
> Accept: */*
> Content-type: application/json
> Content-Length: 30
>
* upload completely sent off: 30 out of 30 bytes
< HTTP/1.1 200 OK
< Server: akka-http/10.1.7
< Date: Tue, 15 Oct 2019 08:46:56 GMT
< Content-Type: application/json
< Content-Length: 18
<
* Connection #0 to host localhost left intact
{"message":"DONE"}* Closing connection 0

EDIT:

Json serialisation must be left to be handled by akka in directives entity(as[]) for request and complete for responses. Do not create JSON manually.

you can try this.

import spray.json.DefaultJsonProtocol
import spray.httpx.unmarshalling._
import spray.httpx.marshalling._
import spray.http._
import HttpCharsets._
import MediaTypes._

case class Person(name: String, firstName: String, age: Int)

object MyJsonProtocol extends DefaultJsonProtocol {
  implicit val PersonFormat = jsonFormat3(Person)
}

import MyJsonProtocol._
import spray.httpx.SprayJsonSupport._
import spray.util._

val bob = Person("Bob", "Parr", 32)
val body = HttpEntity(
  contentType = ContentType(`application/json`, `UTF-8`),
  string =
    """|{
       |  "name": "Bob",
       |  "firstName": "Parr",
       |  "age": 32
       |}""".stripMarginWithNewline("\n")
)

marshal(bob) === Right(body)
body.as[Person] === Right(bob)

Just like jsonFormat1[DeviceData] and as combination is helping you convert a JSON string to DeviceData object, you can use the same technique in the other direction to convert your result to JSON, without manually marshaling the output.

implicit val parseDataJsonFormat = jsonFormatn[ParserManager]
// You should replace jsonFormatn with appropriate number based on the number of fields in ParserManager.
val result = data.hexData.map(row => {
          ParserManager(Hex.decodeHex(row.hexstring.replaceAll("\\s", "").toCharArray))
        }
    )

I did it using following approach -

   case HttpRequest(POST, Uri.Path("/decodedata"), _, entity, _) =>
        val chunk = Unmarshal(entity).to[List[DataChunk]]
        val deviceData = Await.result(chunk, 1.second)
        implicit val formats = org.json4s.DefaultFormats
        val resp = deviceData
          .map(data =>
            Serialization.write(ParserManager(Hex.decodeHex(data.rawData.replaceAll("\\s", "").toCharArray)))
          ).mkString

        HttpResponse(entity = HttpEntity(ContentTypes.`application/json`, resp))



  case class DataChunk(rawData: String)

  object DataChunk {
    implicit val dataChunkJsonFormat: RootJsonFormat[DataChunk] = jsonFormat1(DataChunk.apply)
  }
Related