Akka Http uses the spray-json library for JSON support. But a few days back while working on a project which was using the json4s library for marshalling/unmarshalling, I got stuck during parsing of JSON request and extracting that parsed values into my target Scala data model.
Common transformations are summarized in the following picture.
Description :
- I have a sample Scala web service created using Akka HTTP for performing CRUD operations which is not using any database for the time being as this blog focuses on json4s and Akka HTTP.
- The user input contains objects which I want to extract cleanly. To keep things simpler, I have a Student case class which contains two fields roll number and name of the student. CRUD operations are performed on this Student data model.
Dependencies :
I am using SBT, so here is the relevant code from build.sbt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
version := "0.1" | |
scalaVersion := "2.12.6" | |
libraryDependencies ++= Seq("com.typesafe.akka" %% "akka-http" % "10.0.11", | |
"org.json4s" %% "json4s-native" % "3.2.11", | |
"org.scalatest" %% "scalatest" % "3.0.1" % Test, | |
"org.mockito" % "mockito-core" % "2.11.0" % Test, | |
"com.typesafe.akka" %% "akka-http-testkit" % "10.1.1") |
Scala data model as discussed above :
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import scala.collection.mutable.ListBuffer | |
case class Student(rollNum: Int, name: String) | |
trait Students { | |
val students = ListBuffer(Student(1, "Ayush"), Student(2, "deepankar")) | |
def fetchStudents: ListBuffer[Student] = students | |
def addStudent(student: Student): ListBuffer[Student] = { | |
student +: students | |
} | |
def updateStudent(student: Student): ListBuffer[Student] = { | |
val oldRecord = students.filter(_.rollNum == student.rollNum) | |
students —= oldRecord | |
student +: students | |
} | |
def deleteStudent(rollNum: Int): ListBuffer[Student] = { | |
val deleteRecord = students.filter(_.rollNum == rollNum) | |
students —= deleteRecord | |
} | |
} |
To get Akka HTTP and json4s to play nicely together, I am going to implement a custom trait that defines some helper methods for marshalling/unmarshalling to and from JSON along with necessary implicit values.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import org.json4s.{DefaultFormats, Formats, JNothing, JValue} | |
import org.json4s.native.JsonMethods.{parse => jParser} | |
import org.json4s.native.Serialization | |
import org.json4s.native.Serialization.{write => jWrite} | |
trait JsonHelper extends { | |
val EMPTY_STRING = "" | |
implicit val serialization: Serialization.type = Serialization | |
implicit val formats: Formats = DefaultFormats | |
/* | |
takes any type of value and serializes it to string | |
eg: val student = Student(1,"deepankar") | |
write(student) will serialize it to {"rollNum":1,"name":"deepankar"} | |
*/ | |
def write[T <: AnyRef](value: T): String = jWrite(value) | |
/* | |
Any valid json can be parsed into internal AST(Abstract Syntax Tree) format. | |
eg : If this is our json string coming from a post request | |
val jsonString = {"rollNum":1,"name":"deepankar"} | |
parse(jsonString) will parse it to something like | |
JObject(List((rollNum,JInt(1)), (name,JString(deepankar)))) | |
*/ | |
protected def parse(value: String): JValue = jParser(value) | |
/* | |
extract method is used after parsing the json string, as parse method returns JValue so we | |
can map this JValue to our target scala data model | |
eg : val jsonString = {"rollNum":1,"name":"deepankar"} | |
val parseString = parse(jsonString) | |
extract(parseString) will return Student(?1,"deepankar") | |
*/ | |
implicit protected def extractOrEmptyString(json: JValue): String = { | |
json match { | |
case JNothing => EMPTY_STRING | |
case data => data.extract[String] | |
} | |
} | |
} |
Here’s our sample web server file that handles all CRUD related requests (GET, POST, PUT, DELETE) and responds with JSON. This file extends JsonHelper defined above and also shows the output generated by these helper methods to make your understanding even better.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import akka.actor.ActorSystem | |
import akka.http.scaladsl.Http | |
import akka.http.scaladsl.model._ | |
import akka.http.scaladsl.server.Directives._ | |
import akka.stream.ActorMaterializer | |
import scala.concurrent.ExecutionContextExecutor | |
//the usual config code required for an Akka Http server, mixing in our JsonHelper trait too | |
object Routes extends App with JsonHelper { | |
implicit val system: ActorSystem = ActorSystem("my-system") | |
implicit val materializer: ActorMaterializer = ActorMaterializer() | |
// needed for the future flatMap/onComplete in the end | |
implicit val executionContext: ExecutionContextExecutor = system.dispatcher | |
val studentsClass = new Students | |
val route = | |
pathPrefix("demo") { | |
get { | |
path("fetchAllStudents") { | |
complete { | |
val addedStudents = studentsClass.fetchStudents | |
val jsonResponse = write(addedStudents) | |
/* json for list of Students | |
[{"rollNum":1,"name":"Ayush"},{"rollNum":2,"name":"deepankar"}] | |
*/ | |
HttpResponse(entity = HttpEntity(ContentTypes.`application/json`, jsonResponse)) | |
} | |
} | |
} ~ | |
post { | |
path("addStudent") { | |
entity(as[String]) { studentJson => { | |
complete { | |
val studentRecord = parse(studentJson).extract[studentsClass.Student] | |
/* | |
Student(3,randhir) | |
*/ | |
val updatedList = studentsClass.addStudent(studentRecord) | |
val jsonResponse = write(updatedList) | |
HttpResponse(entity = HttpEntity(ContentTypes.`application/json`, jsonResponse)) | |
} | |
} | |
} | |
} | |
} ~ | |
put { | |
//for updating student name student roll number has to be same as entered previously | |
path("updateStudent") { | |
entity(as[String]) { studentJson => { | |
complete { | |
val studentRecord = parse(studentJson).extract[studentsClass.Student] | |
val updatedList = studentsClass.updateStudent(studentRecord) | |
val jsonResponse = write(updatedList) | |
HttpResponse(entity = HttpEntity(ContentTypes.`application/json`, jsonResponse)) | |
} | |
} | |
} | |
} | |
} ~ | |
delete { | |
path("deleteStudent" / IntNumber) { rollNum => | |
complete { | |
val deleteRecord = studentsClass.deleteStudent(rollNum) | |
val jsonResponse = write(deleteRecord) | |
HttpResponse(entity = HttpEntity(ContentTypes.`application/json`, jsonResponse)) | |
} | |
} | |
} | |
} | |
val bindingFuture = Http().bindAndHandle(route, "localhost", 8080) | |
println(s"Server online at http://localhost:8080") | |
} |
Feel free to download and run the project. In case you face any problem comment below for the help.
Till then keep sharing, reading, blogging.
It seems the code snippets do not render properly.