11

Suppose I have the following case classes that need to be serialized as JSON objects using circe:

@JsonCodec
case class A(a1: String, a2: Option[String])

@JsonCodec
case class B(b1: Option[A], b2: Option[A], b3: Int)

Now I need to encode val b = B(None, Some(A("a", Some("aa")), 5) as JSON but I want to be able to control whether it is output as

{
  "b1": null,
  "b2": {
          "a1": "a",
          "a2": "aa"
        },
  "b3": 5
}

or

{
  "b2": {
          "a1": "a",
          "a2": "aa"
        },
  "b3": 5
}

Using Printer's dropNullKeys config, e.g. b.asJson.noSpaces.copy(dropNullKeys = true) would result in omitting Nones from output whereas setting it to false would encode Nones as null (see also this question). But how can one control this setting on a per field basis?

Community
  • 1
  • 1
msilb
  • 415
  • 4
  • 14

2 Answers2

18

The best way to do this is probably just to add a post-processing step to a semi-automatically derived encoder for B:

import io.circe.{ Decoder, JsonObject, ObjectEncoder }
import io.circe.generic.JsonCodec
import io.circe.generic.semiauto.{ deriveDecoder, deriveEncoder }

@JsonCodec
case class A(a1: String, a2: Option[String])
case class B(b1: Option[A], b2: Option[A], b3: Int)

object B {
  implicit val decodeB: Decoder[B] = deriveDecoder[B]
  implicit val encodeB: ObjectEncoder[B] = deriveEncoder[B].mapJsonObject(
    _.filter {
      case ("b1", value) => !value.isNull
      case _ => true
    }
  )
}

And then:

scala> import io.circe.syntax._
import io.circe.syntax._

scala> B(None, None, 1).asJson.noSpaces
res0: String = {"b2":null,"b3":1}

You can adjust the argument to the filter to remove whichever null-valued fields you want from the JSON object (here I'm just removing b1 in B).

It's worth noting that currently you can't combine the @JsonCodec annotation and an explicitly defined instance in the companion object. This isn't an inherent limitation of the annotation—we could check the companion object for "overriding" instances during the macro expansion, but doing so would make the implementation substantially more complicated (right now it's quite simple). The workaround is pretty simple (just use deriveDecoder explicitly), but of course we'd be happy to consider an issue requesting support for mixing and matching @JsonCodec and explicit instances.

Travis Brown
  • 135,682
  • 12
  • 352
  • 654
  • Thanks, this works great for the use case. Actually, what I really need is a way to let user of the library be able to control whether the value is 'omitted' from json or serialized as null. I guess my only option is to create some sort of ADT to map out 3 possible outputs: `None` (omitted), `Some(ANullValue)` or `Some(A(...))` and then use custom encoder as you suggested. – msilb Feb 22 '17 at 08:52
  • 1
    @msilb where you able to come up with a custom encoder for your use-case? I would love to see and example. – osocron Aug 12 '20 at 13:39
2

Circe have added a method dropNullValues on Json that uses what Travis Brown mentioned above.

def dropNulls[A](encoder: Encoder[A]): Encoder[A] =
    encoder.mapJson(_.dropNullValues)

implicit val entityEncoder: Encoder[Entity] = dropNulls(deriveEncoder)
annedroiid
  • 4,071
  • 8
  • 25
  • 49