LoginSignup
15
11

More than 5 years have passed since last update.

[Akka-HTTP]spray-jsonの使い方とAkka-HTTP

Posted at

Akka-HTTPで公式にサポートされているJSONライブラリのspray-jsonの使い方。
JSON Support — Akka Documentation
Akka-HTTPと一緒に使いたいので、Marshaller/Unmarshallerとして使用法も載せる。

tl;dr

  • クラスのフィールドに独自の型を使わない場合 or 独自の型に対応させてjsonを入れ子にする場合
    • jsonFormatNを使えば手軽で良い
  • クラスのフィールドとjsonのフォーマットを変えたい場合
    • RootJsonProtocolを一生懸命実装する
  • Akka-HTTPではSprayJsonSupportをextendsすればimplicitにやってくれる
    • akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport

基本的な使い方

case classをjsonに変換、再変換を行う。

case class User(id: Long, name: String)

このcase classに対応するRootJsonFormatを用意すればよい。
DefaultJsonProtocolをextendsしたobject内で実装するとこうなる。

object UserJsonProtocol extends DefaultJsonProtocol {
  implicit val format = jsonFormat2(User.apply)
}

これを使ってjson <-> case classをするとこんな感じ。

import UserJsonProtocol._

// case class -> json
val user = User(100, "alice")
user.toJson  // {"id":100,"name":"alice"}

// json -> case class
val json = "{\"id\":10,\"name\":\"bob\"}"
json.parseJson.convertTo[User]  // User(10,bob)

ここで使ったjsonFormat2Userクラスのコンストラクタ引数が2つであることに対応している
jsonFormatNを使用するとフィールド名(コンストラクタの仮引数名)がjsonのkeyとなる。
フィールド名とjsonのkeyを変えたい場合はjsonFormatを使う。

implicit val format: RootJsonFormat[User] = jsonFormat(User.apply, "user_id", "user_name")

こう書くとコンストラクタに対して順番にuser_id, user_nameを割り当てることとなる。
つまり、この様にjson <-> case classの関係が変化する。

// case class -> json
val user = User(100, "alice")
user.toJson  // {"user_id":100,"user_name":"alice"}

// json -> case class
val json = "{\"user_id\":10,\"user_name\":\"bob\"}"
json.parseJson.convertTo[User]  // User(10,bob)

akka-httpで使う

akka-httpとspray-jsonを組み合わせる。
具体的にはToResponseMarshallerとしてspray-jsonを使うこととなる。
completeにクラスTのオブジェクトを渡してjsonとして返却したい場合、 implicitなRootJsonProtocol[T]をスコープ内に用意しておけば良い。
変換がcase classからjsonへの一方向のみの場合、RootJsonWriterだけでも可。
Marshalling/Unmarshallingとjson<->case classの対応関係は以下。

  • json -> case classがUnmarshalling
    • RootJsonReaderが必要
  • case class -> jsonがMarshalling
    • RootJsonWriterが必要

controller的なところにSprayJsonSupportをextendsしておけばimplicitにやってくれる。

import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport

// jsonで返したい型
case class User(id: Long, name: String)

// implicitなRootJsonProtocol[User]を用意
implicit val format: RootJsonProtocol[User] = jsonFormat2(User.apply)

val route =
  // GET /user/<name>
  path("user" / ".+".r) { name =>
    val id: Long = new Random().nextInt(100).toLong
    val user: User = User(id, name)

    complete(user)
  }

リクエストとレスポンスはこのようになる。
Content-Type: application/jsonとなっており、bodyもjsonになっている。

$ curl localhost:8080/user/alice -i
HTTP/1.1 200 OK
Server: akka-http/2.4.2
Date: Tue, 03 May 2016 10:30:40 GMT
Content-Type: application/json
Content-Length: 33

{
  "id": 90,
  "name": "alice"
}

やや複雑な例

前述の例ではクラスのフィールドの型がいわゆるプリミティブ型だけのものであった。
次は複雑なオブジェクト、クラスのフィールドの型が独自クラスになっているような場合を考える。

クラス定義

例として使うクラス定義を以下に示す。

// IDにまつわる型
trait Identifier[+A] extends Any {
  def value: A
}
object Identifier {
  type IdType = String
}
case class ID[A <: Entity[_]](value: IdType) extends AnyVal with Identifier[IdType]

// Userの親となるEntity
trait Entity[Id <: Identifier[_]] {
  val id: Id
}

// Userのフィールド型
case class Name(value: String) extends AnyVal
case class Email(value: String) extends AnyVal

case class User(id: ID[User], name: Name, email: Email) extends Entity[ID[User]]

単純なRootJsonProtocolを実装

定義したUserクラスをjsonに変換するためのRootJsonProtocolを実装する。
RootJsonProtocol[User]を実装するためには、各フィールドの型に応じたRootJsonProtocolが必要となる。

object UserJsonProtocol extends DefaultJsonProtocol {
  implicit val userIdFormat = jsonFormat1(ID.apply[User])
  implicit val nameFormat = jsonFormat1(Name.apply)
  implicit val emailFormat = jsonFormat1(Email.apply)
  implicit val userFormat = jsonFormat3(User.apply)
}

このRootJsonProtocol群を使ってUserクラスのオブジェクトをjsonに変換してみる。

import UserJsonProtocol._
val user = User(ID("alice-id"), Name("alice"), Email("alice@example.com"))
user.toJson

結果として以下の様なjsonが得られる。

{
  "id": {
    "value": "alice-id"
  },
  "name": {
    "value": "alice"
  },
  "email": {
    "value": "alice@example.com"
  }
}

入れ子になってしまっているが、オブジェクトをクラス定義に沿って正しくserializeしていると言える。

akka-httpでの使用

上記のjsonFormatNを使ったRootJsonFormatの実装だと、なぜかakka-httpではうまくいかず以下の様なエラーメッセージが出力されてレスポンスは返ってこない。

Caused by: java.lang.RuntimeException: Cannot automatically determine case class field names and order for 'net.petitviolet.domain.user.Name', please use the 'jsonFormat' overload with explicit field name specification

メッセージにある通り、明示的にjsonFormatを使う。

object UserJsonProtocol extends DefaultJsonProtocol {
  implicit val userIdFormat = jsonFormat(ID.apply[User] _, "value")
  implicit val nameFormat = jsonFormat(Name.apply _, "value")
  implicit val emailFormat = jsonFormat(Email.apply _, "value")
  implicit val userFormat = jsonFormat(User.apply, "id", "name", "email")
}

これで上と同様なjsonが返ってくるようになる。

フラットなjsonを作る

入れ子になったクラスをserializeすると入れ子になったjsonが返ってきた。
入れ子になったvalueが必要でない場合、jsonFormatNを使った実装では実現できない。
そのため、RootJsonProtocol[User]を自前で実装することとなる。

object UserJsonProtocol extends DefaultJsonProtocol {
  implicit val userFormat: RootJsonFormat[User] = new RootJsonFormat[User] {
    override def read(json: JsValue): User =
      json.asJsObject.getFields("id", "name", "email") match {
        case Seq(JsString(id), JsString(name), JsString(email)) =>
          User(ID(id), Name(name), Email(email))
        case _ => throw new DeserializationException("User")
      }

    override def write(user: User): JsValue = JsObject(
      "id" -> JsString(user.id.value),
      "name" -> JsString(user.name.value),
      "email" -> JsString(user.email.value)
    )
  }
}

これを使ってUserをjsonに変換するとフラットなjsonが得られる。

{
  "id": "alice-id",
  "name": "alice",
  "email": "alice@example.com"
}
15
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
11