Circeでエンコード時にnullではなくフィールドごと削除する方法

Circeのエンコードでちょっとハマって時間を溶かしたので備忘録として書いておきます。

やりたいこと

あるクラスにOption型のフィールドが含まれていて、そのフィールドが存在しない(Noneである)場合はエンコード時にそのフィールドをnullではなくフィールドごと削除したい。

可能であれば余計な記述は少なくしたい。

{
  "id" : 1,
  "name" : "ryskit"
  "age": null ← 値がない場合はこのフィールドを削除したい
}

解決策

その1

以下のように オブジェクト Response 内で io.circe.Encode トレイトを実装する。

これでも実現できますが、わざわざ書くのが面倒です。

package app

import io.circe.{Encoder, Json}
import io.circe.generic.semiauto.deriveEncoder
import io.circe.syntax._

case class Response(id: Int, name: String, age: Option[Int])

object Response {
  implicit val encoder: Encoder[Response] = new Encoder[Response] {
    private val generated = deriveEncoder[Response]
    override def apply(a: Response): Json =
      generated(a).mapObject(_.filter {
        case (_, v) =>
          !v.isNull
      })
  }
}

object Main extends App {
  val response = Response(1, "ryskit", None)
  val responseJson = response.asJson.spaces2
  println(responseJson)
}

その2 (最終的な解決策)

その1と同じように、オブジェクト Response 内で encoderを定義します。

Encoderを実装せずにio.circe.generic.semiauto.deriveEncoderを使用し、dropNullValuesでフィールドを削除します。

これで記述量も少なくなります。

package app

import io.circe.Encoder
import io.circe.generic.semiauto.deriveEncoder
import io.circe.syntax._

case class Response(id: Int, name: String, age: Option[Int])

object Response {
  implicit val encoder: Encoder[Response] = deriveEncoder[Response].mapJson(_.dropNullValues)
}

object Main extends App {
  val response = Response(1, "ryskit", None)
  val responseJson = response.asJson.spaces2
  println(responseJson)
}

github.com