Akka-HTTP - spray-jsonの使い方とAkka-HTTP
2016-06-02
QiitaScalaAkka-HTTPAkka-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)
ここで使ったjsonFormat2
はUser
クラスのコンストラクタ引数が 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"
}
from: https://qiita.com/petitviolet/items/79f2bd3b4f1d54d38db1