はじめに
本投稿はScalaのJSONライブラリcirceの基本の続きです
今回は手動でエンコード、デコードする部分に焦点を当てて書いていきます
概要
今回は以下の3点を見ていく
- Encodeのカスタマイズ
- Decodeのカスタマイズ
- キーのカスタマイズ
Encodeのカスタマイズ
基本は以下で、PlayのJson APIと大差ない感じ
import io.circe._
case class User(name: UseName, age: Int, address: String)
case class UseName(firstName: String, lastName: String)
implicit val encodeUser: Encoder[User] = Encoder.instance { user =>
Json.obj(
"firstName" -> Json.fromString(user.name.firstName),
"lastName" -> Json.fromString(user.name.lastName),
"information" -> Json.obj(
"age" -> Json.fromInt(user.age),
"address" -> Json.fromString(user.address)
)
)
}
val user1 = User(UseName("foo", "bar"), 3, "buz")
println(user1.asJson)
// {
// "firstName" : "foo",
// "lastName" : "bar",
// "information" : {
// "age" : 3,
// "address" : "buz"
// }
// }
別の方法としては以下のようにメソッドチェーンで記述可能
結果は全く同じ
implicit val encodeUser: Encoder[User] = Encoder.instance { user =>
Json.fromJsonObject(
JsonObject.empty
.add("firstName", Json.fromString(user.name.firstName))
.add("lastName", Json.fromString(user.name.lastName))
.add(
"information",
Json.fromJsonObject(
JsonObject.empty
.add("age", Json.fromInt(user.age))
.add("address", Json.fromString(user.address))
)
)
)
}
2つ目の方法は今回は冗長になってしまったが、適切な使い方であればしっかり効果を発揮してくれそう
例として、name
だけを変えたい場合を考える
implicit val encodeUser: Encoder[User] = Encoder.instance { user =>
Json.fromJsonObject(
user.asJsonObject
.add("firstName", Json.fromString(user.name.firstName))
.add("lastName", Json.fromString(user.name.lastName))
.remove("name")
)
}
println(user1.asJson)
// {
// "age" : 3,
// "address" : "buz",
// "firstName" : "foo",
// "lastName" : "bar"
// }
こんな具合に一部だけを変えたい場合だとスッキリかけそう
Decodeのカスタマイズ
JSONが自動デコードの形にそぐわない場合は以下のように個別にDecoderを定義する
implicit val decodeUser: Decoder[User] = Decoder.instance { cursor =>
for {
firstName <- cursor.downField("firstName").as[String]
lastName <- cursor.downField("lastName").as[String]
age <- cursor.downField("age").as[Int]
address <- cursor.downField("address").as[String]
} yield User(UseName(firstName,lastName), age, address)
}
val userJson = """
{
"age" : 3,
"address" : "buz",
"firstName" : "foo",
"lastName" : "bar"
}"""
val user = decode[User](userJson)
println(user)
// => Right(User(UseName(foo,bar),3,buz))
cursor
を使って個別に値を取得し、マッピングするだけ
ただし、ゴツいコードにはなるのでデコード専用のcase classを定義して自動デコードをフル活用するのも手かと思う
アーキテクチャによってはリクエストの中身をそのままモデルにマッピングしない方法を取ることもあると思うので(バリデーションを挟んだりするなど)、その場合はこっちのほうがむしろいいかもしれない
以下のイメージ(toUser
はとりあえず定義しただけです)
import io.circe.generic.auto._
case class UserJson(firstName: String, lastName: String, age: Int, address: String) {
def toUser: User =
User(UseName(this.firstName, this.lastName), this.age, this.address)
}
val user = decode[UserJson](userJson).map(_.toUser)
println(user)
// => Right(User(UseName(foo,bar),3,buz))
キーのカスタマイズ
デコードについてはもう少し細かくキーだけをピンポイントでカスタマイズできる方法がいくつかある
Map型を使ったカスタマイズ
クラスの値をそのままJSONのキーにしたい場合に使える
import io.circe._, io.circe.syntax._
case class Hoge(value: String)
implicit val hogeKeyEncoder: KeyEncoder[Hoge] = KeyEncoder.instance(_.value)
val map = Map(
Hoge("foo") -> "hogehoge"
)
val mapJson = map.asJson
println(mapJson)
// {
// "foo" : "hogehoge"
// }
implicit val hogeKeyDecoder: KeyDecoder[Hoge] = KeyDecoder.instance(key => Some(Hoge(key)))
println(mapJson.as[Map[Hoge, String]])
// Right(Map(Hoge(foo) -> hogehoge))
forProductNを使ったカスタマイズ
forProductN
というメソッドが用意されており、これを使う
とりあえずコードを見てみたほうがわかりやすい
import io.circe._, io.circe.syntax._
implicit val encodeUserName: Encoder[UseName] =
Encoder.forProduct2("first_name", "last_name")(u => (u.firstName, u.lastName))
val userName = UserName("foo", "bar")
println(userName.asJson)
// {
// "first_name" : "foo",
// "last_name" : "bar"
// }
ちなみにNは22まで対応している
アノテーションを使ったカスタマイズ
アノテーションを使ってカスタマイズする方法も一応ドキュメントでは紹介されているが、なぜか手元で動いてくれなかったので省略します…
終わりに
今回までの内容さえ抑えられれば基本的な対応はできるようになっているとは思います。まだまだ応用的な内容もあるっぽいですが、ここを理解しているのとそうでないのでは違ってくるはずです。本投稿は備忘的な役割が大半ですが、circeをかじってみたい方の助けになっていれば幸いです。