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)
ここで使った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"
}