JSON mit Akka HTTP

Keine Kommentare

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:

case class Customer(
  id: UUID,
  name: String,
  registrationDate: LocalDate,
  gender: Gender,
  customerType: CustomerType,
  addresses: Option[Set[Address]]
)
case class Address(
  street: String,
  city: String,
  zip: String,
  active: Boolean = false
)

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.

// Main.scala
object CustomerType extends Enumeration {
  type CustomerType = Value
  val Regular, Vip = Value
}

Gender wird als sealed Trait umgesetzt.

sealed trait Gender
case object Female extends Gender
case 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.

object Main extends App {
  import domain._
 
  implicit private val system = ActorSystem()
  implicit private val mat    = ActorMaterializer()
 
  // Some dummy data
  private val uuid1 = UUID.fromString("5919d228-9abf-11e6-9f33-a24fc0d9649c")
  private val uuid2 = UUID.fromString("660f7186-9abf-11e6-9f33-a24fc0d9649c")
  private val uuid3 = UUID.fromString("70d0d722-9abf-11e6-9f33-a24fc0d9649c")
 
  private val address1 = Address("Musterstrasse 2", "Musterstadt", "12345")
  private val address2 =
    Address("Testplatz 80 5", "Musterhausen", "45789", active = true)
  private val address3 =
    Address("Akka-Allee 1887", "Akkaburg", "61860", active = true)
 
  private var customers =
    Map(
      uuid1 -> Customer(uuid1,
                        "test1",
                        LocalDate.of(2010, 1, 11),
                        Female,
                        CustomerType.VIP,
                        None),
      uuid2 -> Customer(uuid2,
                        "test2",
                        LocalDate.of(2014, 6, 5),
                        Male,
                        CustomerType.VIP,
                        Some(Set(address1, address2))),
      uuid3 -> Customer(uuid3,
                        "test3",
                        LocalDate.of(2012, 2, 25),
                        Female,
                        CustomerType.REGULAR,
                        Some(Set(address3)))
    )
 
  private def route = {
    import Directives._
    pathPrefix("customer") {
      post {
        entity(as[Customer]) { customer =>
          println(customer)
          customers += customer.id -> customer
          complete(customers)
        }
      } ~
      path(JavaUUID) { id =>
        get {
          complete(customers(id))
        }
      }
    }
  }
 
  import system.dispatcher
  Http().bindAndHandle(route, "localhost", 8080).onComplete {
    case Failure(cause) =>
      println(s"Can't bind to localhost:8000: $cause")
      system.terminate()
    case _ =>
      println(s"Server online at http://localhost:8080")
  }
  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:

libraryDependencies += "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.

trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol {
  implicit val addressFormat = jsonFormat4(Address)
  implicit val customerFormat = jsonFormat6(Customer)
}

Der Trait JsonSupport wird in Main.scala hinein gemixt:

object 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.

 trait CustomerJsonProtocol extends DefaultJsonProtocol {

implicit val uuidJsonFormat: JsonFormat[UUID] = new JsonFormat[UUID] {
override def write(x: UUID): JsValue = JsString(x.toString)

  • Seite
  • 1
  • 2

Tags

Christian Hof

Christian Hof beschäftigt sich mit Big Data und das am liebsten innerhalb des Hadoop-Ökosystems. Dabei spielen agile Methoden und Testautomatisierung eine wichtige Rolle.

Share on FacebookGoogle+Share on LinkedInTweet about this on TwitterShare on RedditDigg thisShare on StumbleUpon

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.