4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ゴリゴリcirceをカスタマイズしていく

Last updated at Posted at 2020-08-07

はじめに

本投稿はScalaのJSONライブラリcirceの基本の続きです
今回は手動でエンコード、デコードする部分に焦点を当てて書いていきます

概要

今回は以下の3点を見ていく

  1. Encodeのカスタマイズ
  2. Decodeのカスタマイズ
  3. キーのカスタマイズ

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をかじってみたい方の助けになっていれば幸いです。

4
2
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
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?