In diesem Artikel wird die Nutzung von JSON mit Akka HTTP thematisiert.
Dabei verfolgen wir zunächst eine Umsetzung, die sich an der aktuellen Dokumentation von Akka orientiert und nutzen spray-json . Im Anschluss zeigen wir einen alternativen Ansatz mit akka-http-json und circe .
Die Domäne
Die Domäne wird hier bewusst einfach gehalten, da der Fokus auf Parsing und Mapping liegt:
1case class Customer(
2 id: UUID,
3 name: String,
4 registrationDate: LocalDate,
5 gender: Gender,
6 customerType: CustomerType,
7 addresses: Option[Set[Address]]
8)
9case class Address(
10 street: String,
11 city: String,
12 zip: String,
13 active: Boolean = false
14)
Neben diesen Case-Classes existieren noch die Aufzählungstypen Gender und CustomerType. Obwohl es nachteilig ist, Aufzählungen als Erweiterung von Enumeration zu implementieren (ein Artikel dazu hier ), wird CustomerType hier so umgesetzt, um alle Eventualitäten abzudecken. Ein Grund dafür kann bspw. der Umgang mit Legacy-Code sein.
1// Main.scala
2object CustomerType extends Enumeration {
3 type CustomerType = Value
4 val Regular, Vip = Value
5}
Gender wird als sealed Trait umgesetzt.
1sealed trait Gender
2case object Female extends Gender
3case object Male extends Gender
Server
Für das Beispiel existiert ein einfacher Server, dessen Routing durch paths definiert wird. POST-Requests werden unter http://localhost:8080/customer entgegengenommen. Das übertragene Objekt wird zunächst in der Konsole ausgegeben, anschließend customers hinzugefügt und schließlich wird die aktualisierte Liste hinzugefügt. GET liefert für eine bestimmte UUID das entsprechende Element der Map zurück.
1object Main extends App {
2 import domain._
3
4 implicit private val system = ActorSystem()
5 implicit private val mat = ActorMaterializer()
6
7 // Some dummy data
8 private val uuid1 = UUID.fromString("5919d228-9abf-11e6-9f33-a24fc0d9649c")
9 private val uuid2 = UUID.fromString("660f7186-9abf-11e6-9f33-a24fc0d9649c")
10 private val uuid3 = UUID.fromString("70d0d722-9abf-11e6-9f33-a24fc0d9649c")
11
12 private val address1 = Address("Musterstrasse 2", "Musterstadt", "12345")
13 private val address2 =
14 Address("Testplatz 80 5", "Musterhausen", "45789", active = true)
15 private val address3 =
16 Address("Akka-Allee 1887", "Akkaburg", "61860", active = true)
17
18 private var customers =
19 Map(
20 uuid1 -> Customer(uuid1,
21 "test1",
22 LocalDate.of(2010, 1, 11),
23 Female,
24 CustomerType.VIP,
25 None),
26 uuid2 -> Customer(uuid2,
27 "test2",
28 LocalDate.of(2014, 6, 5),
29 Male,
30 CustomerType.VIP,
31 Some(Set(address1, address2))),
32 uuid3 -> Customer(uuid3,
33 "test3",
34 LocalDate.of(2012, 2, 25),
35 Female,
36 CustomerType.REGULAR,
37 Some(Set(address3)))
38 )
39
40 private def route = {
41 import Directives._
42 pathPrefix("customer") {
43 post {
44 entity(as[Customer]) { customer =>
45 println(customer)
46 customers += customer.id -> customer
47 complete(customers)
48 }
49 } ~
50 path(JavaUUID) { id =>
51 get {
52 complete(customers(id))
53 }
54 }
55 }
56 }
57
58 import system.dispatcher
59 Http().bindAndHandle(route, "localhost", 8080).onComplete {
60 case Failure(cause) =>
61 println(s"Can't bind to localhost:8000: $cause")
62 system.terminate()
63 case _ =>
64 println(s"Server online at http://localhost:8080")
65 }
66 Await.ready(system.whenTerminated, Duration.Inf)
JSON-Support mit spray-json
Dieser Abschnitt orientiert sich an der Dokumentation zur Nutzung von JSON im Kontext von Akka HTTP 2.4.11. Um spray-json im Beispielprojekt einzubinden, wird folgende Dependency verwendet:
1libraryDependencies += "com.typesafe.akka" %% "akka-http-spray-json-experimental" % "2.4.11"
Um den Server JSON-fähig zu gestalten, bedarf es noch der Einbindungen der notwendigen Kapazitäten für spray-json sowie der Definition eines JSON-Protokolls. Dazu werden im Trait JsonSupport das JSON-Format für Address und Customer definiert. Ersteres wird mit dem Aufruf jsonFormat4(Address) erreicht – die vier steht für die Anzahl der Parameter im Konstruktor. Das Format für Customer wird äquivalent behandelt.
1trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol {
2 implicit val addressFormat = jsonFormat4(Address)
3 implicit val customerFormat = jsonFormat6(Customer)
4}
Der Trait JsonSupport wird in Main.scala hinein gemixt:
1object Main extends App with JsonSupport { //...
Custom-Types
Grundsätzlich würden diese Implementierungsschritte ausreichen, jedoch deckt DefaultJsonProtocol nicht alle Typen ab, die hier verwendet werden. Im vorliegenden Fall benötigen wir Formate für:
- java.util.UUID
- java.time.LocalDate
- oben beschriebene Aufzählungen
Dazu wird das im Trait CustomerJsonProtocol für jedes benötige Format ein impliziter Wert des Types JsonFormat angelegt und dessen Methoden read und write implementiert.
1trait CustomerJsonProtocol extends DefaultJsonProtocol {
2
3 implicit val uuidJsonFormat: JsonFormat[UUID] = new JsonFormat[UUID] {
4 override def write(x: UUID): JsValue = JsString(x.toString)
5
6 override def read(value: JsValue): UUID = value match {
7 case JsString(x) => UUID.fromString(x)
8 case x => deserializationError("Expected UUID as JsString, but got " + x)
9 }
10 }
11
12 implicit val localDateJsonFormat: JsonFormat[LocalDate] =
13 new JsonFormat[LocalDate] {
14 private val formatter = DateTimeFormatter.ISO_DATE
15 override def write(x: LocalDate): JsValue = JsString(x.format(formatter))
16
17 override def read(value: JsValue): LocalDate = value match {
18 case JsString(x) => LocalDate.parse(x)
19 case x => deserializationError("Wrong time format of " + x)
20 }
21 }
22
23 implicit val customerTypeFormat: JsonFormat[CustomerType] =
24 new JsonFormat[CustomerType] {
25 override def write(x: CustomerType): JsValue = JsString(x.toString)
26
27 override def read(value: JsValue): CustomerType = value match {
28 case JsString("REGULAR") => CustomerType.REGULAR
29 case JsString("VIP") => CustomerType.VIP
30 case x => deserializationError("No CustomerType with name " + x)
31 }
32 }
33
34 implicit val genderFormat: JsonFormat[Gender] = new JsonFormat[Gender] {
35 override def write(x: Gender): JsValue = JsString(x.toString.toUpperCase)
36
37 override def read(value: JsValue): Gender = value match {
38 case JsString("MALE") => Male
39 case JsString("FEMALE") => Female
40 case x => deserializationError("Not a Gender " + x)
41 }
42 }
43}
Im Trait JsonSupport wird dieser Trait anstelle von DefaultJsonProtocol verwendet:
1trait JsonSupport extends SprayJsonSupport with CustomerJsonProtocol {//...
JSON mit akka-http-json und circe
Alternativ zu spray-json existieren weitere Bibliotheken, um JSON im Kontext von Akka HTTP zu nutzen. Eine davon ist akka-http-json, die hier mit circe verwendet wird. Dazu wird folgende Dependency (anstelle der Dependency für spray-json) eingebunden:
1libraryDependencies ++= List( 2 "de.heikoseeberger" %% "akka-http-circe" % "1.10.1", 3 "io.circe" %% "circe-generic" % "0.5.2", 4 "io.circe" %% "circe-java8" % "0.5.2"</ul> 5)
Die erste Dependency bindet die Integration von circe in Akka HTTP, die maßgeblich mittels CirceSupport zur Verfügung gestellt wird, ein. circe-generic ermöglicht die automatische Erzeugung von Codecs (analog Formaten bei spary-json) zur Compile-Time. circe-java8 beinhalten Codecs für spezielle Typen von Java 8, insbesondere LocalDate.
Um also circe zu nutzen, wird Main.scala folgendermaßen angepasst:
1// more imports
2import de.heikoseeberger.akkahttpcirce.CirceSupport
3
4object Main extends App with CirceSupport {
5 import io.circe.generic.auto._
6 import io.circe.java8.time._
7 //...
Im Companion Object für CustomerType werden Decoder und Encoder als implizite Werte angelegt:
1object CustomerType extends Enumeration {
2 type CustomerType = Value
3 val Regular, Vip = Value
4
5 implicit val customerTypeDecoder: Decoder[CustomerType] =
6 new Decoder[CustomerType] {
7 override def apply(c: HCursor) =
8 c.as[String].flatMap{ x =>
9 Xor
10 .catchOnly[NoSuchElementException](CustomerType.withName(x))
11 .leftMap(e => DecodingFailure(s"Error while decoding: ${e.getMessage}", List()))
12 }
13 }
14
15 implicit val customerTypeEncoder: Encoder[CustomerType] =
16 new Encoder[CustomerType] {
17 override def apply(`type`: CustomerType) =
18 Json.fromString(`type`.toString)
19 }
20}
Für Gender wird analog vorgegangen. In der vorliegende Version (0.5.2) von circe, die hier verwendet wird, sollten ADTs automatisch, jedoch gab es hierbei Probleme, die nicht gelöst werden konnten. Außerdem weicht der JSON-Output ab, so dass ein eigener Codec benötigt wird. Mit kann circe 0.6 kann dieser aber konfiguriert werden.
1sealed trait Gender
2case object Female extends Gender
3case object Male extends Gender
4
5object Gender {
6
7 implicit val genderDecoder: Decoder[Gender] =
8 new Decoder[Gender] {
9 override def apply(c: HCursor) = c.as[String].flatMap {
10 case "FEMALE" => Xor.right(Female)
11 case "MALE" => Xor.right(Male)
12 case x => Xor.left(DecodingFailure(s"Not a gender $x", List()))
13 }
14 }
15
16 implicit val genderEncoder: Encoder[Gender] =
17 new Encoder[Gender] {
18 override def apply(a: Gender) = Json.fromString(a.toString.toUpperCase)
19 }
20}
Für java.util.UUID sind dank circe-java8 bereits Mechanismen vorhanden und müssen nicht wie bei spray-json implementiert werden.
Fazit
JSON kann mit wenig Aufwand in einem auf Akka HTTP basierenden Server eingebunden werden. Ob nun spray-json, circe oder eine andere Bibliothek verwendet wird, hängt nicht nur vom Gusto des Entwicklers ab, sondern selbstverständlich von der Qualität, dem Funktionsumfang sowie wie die Pflege und Weiterentwicklung der jeweiligen Bibliothek. Persönlich erscheint circe umfangreicher als spray-json zu sein.
Das Repository mit den Projekten für circe und spray-json befindet sich hier .
Weitere Beiträge
von Christian Börner-Schulte
Dein Job bei codecentric?
Jobs
Agile Developer und Consultant (w/d/m)
Alle Standorte
Weitere Artikel in diesem Themenbereich
Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.
Gemeinsam bessere Projekte umsetzen.
Wir helfen deinem Unternehmen.
Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.
Hilf uns, noch besser zu werden.
Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.
Blog-Autor*in
Christian Börner-Schulte
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.