Search code examples
scalacirce

Circe: force an optional field to `null`


Is there a way to serialize a single None field to "null" ?

For example:

// When None, I'd like to serialize only f2 to `null`
case class Example(f1: Option[Int], f2: Option[Int])
val printer = Printer.noSpaces.copy(dropNullValues = true)

Example(None, None).asJson.pretty(printer) === """{"f2":null}"""

Solution

  • You can do this pretty straightforwardly by mapping a filter over the output of an encoder (which can be derived, defined with Encoder.forProductN, etc.):

    import io.circe.{ Json, ObjectEncoder }
    import io.circe.generic.semiauto.deriveEncoder
    
    case class Example(f1: Option[Int], f2: Option[Int])
    
    val keepSomeNulls: ((String, Json)) => Boolean = {
      case ("f1", v) => !v.isNull
      case (_, _) => true
    }
    
    implicit val encodeExample: ObjectEncoder[Example] =
      deriveEncoder[Example].mapJsonObject(_.filter(keepSomeNulls))
    

    And then:

    scala> import io.circe.syntax._
    import io.circe.syntax._
    
    scala> Example(Some(1), Some(2)).asJson.noSpaces
    res0: String = {"f1":1,"f2":2}
    
    scala> Example(Some(1), None).asJson.noSpaces
    res1: String = {"f1":1,"f2":null}
    
    scala> Example(None, Some(2)).asJson.noSpaces
    res2: String = {"f2":2}
    
    scala> Example(None, None).asJson.noSpaces
    res3: String = {"f2":null}
    

    Note that configuring a printer to drop null values will still remove "f2": null here. This is part of the reason I think in general it's best to make the preservation of null values solely the responsibility of the printer, but in a case like this, the presence or absence of null-valued fields is clearly semantically meaningful, so you kind of have to mix up the levels.