PhD
PhD

Reputation: 11334

How to create custom encoding of Option types with Circe?

It's possible to have a class that looks like so:

case class Amount(value: Int)
case class Data(insurance: Option[Amount], itemPrice: Amount)

If insurance = None it should get a default value of waived: true

E.g:

Data(Some(123),100).asJson

// output
{
  "insurance": {
    "value": 123
  },
  "price": 100
}

And when no Insurance is opted for:

Data(None,100).asJson

// output
{
  "insurance": {
    "waived: true
  },
  "price": 100
}

How can this fine-grained control be achieved? I tried various tricks with forProduct2 and mapJsonObject but couldn't get it to behave right:

implicit val testEncoder = deriveEncoder[Option[Amount]].mapJsonObject(j => {

    val x = j("Some") match {
      case Some(s) => // need to convert to [amount -> "value"]
      case None => JsonObject.apply(("waived",Json.fromBoolean(true)))
    }

    x
  })

This can easily get me the waived:true part but no idea how to handle the Some(s) case.

Upvotes: 4

Views: 2415

Answers (1)

Andrey Patseev
Andrey Patseev

Reputation: 524

If having {"waived": true} is expected behavior for any Option[Amount] if it's None, then you can rely on semiauto derived encoders if you write your custom encoder for Option[Amount]

Here is an example

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

case class Amount(value: Int)
case class Data(insurance: Option[Amount], itemPrice: Amount)

object Amount {
  implicit val encoder: Encoder[Amount] = deriveEncoder
}

object Data {
  implicit val encoderOptionalAmount: Encoder[Option[Amount]] = (optA: Option[Amount]) =>
      optA match {
        case Some(amount) => amount.asJson
        case None => Json.obj("waived" -> true.asJson)
      }

  implicit val encoder: Encoder[Data] = deriveEncoder[Data]
}

println(Data(insurance = None, itemPrice = Amount(10)).asJson)

/*
{
  "insurance" : {
    "waived" : true
  },
  "itemPrice" : {
    "value" : 10
  }
}
*/

How it works: deriveEncoder[Data] will call implicit encoders for both itemPrice (of type Amount) and insurance of type Option[Amount].

Default encoder for Option[T] just skips the value if it's None but since we defined another implicit encoder for Option[T] in the closest scope (Data object-companion) it won't look for implicit encoders in global scopes giving you exactly what you want.

Upvotes: 3

Related Questions