Lasf
Lasf

Reputation: 2582

How to write a custom decoder for [Option[Option[A]] in Circe?

I had written a Reads converter in play-json for Option[Option[A]] that had the following behavior:

//given this case class
case class MyModel(field: Option[Option[String]])

//this JSON -- maps to --> this MyModel:
//"{ \"field\": \"value\" }"  -->  MyModel(field = Some(Some("value")))
//"{ \"field\": null, ...  }"   -->  MyModel(field = Some(None))
//"{ }"  -->  MyModel(field = None)

So, providing the value mapped to Some[Some[A]], providing null mapped to Some[None] (i.e. Some[Option.empty[A]]), and not providing the value mapped to just None (i.e. Option.empty[Option[A]]). Here's the play-json converter:

def readOptOpt[A](implicit r: Reads[A]): Reads[Option[Option[A]]] = {
  Reads[Option[Option[A]]] { json =>
    path.applyTillLast(json).fold(
      identity,
      _.fold(_ => JsSuccess(None), {
        case JsNull => JsSuccess(Some(None))
        case js => r.reads(js).repath(path).map(a => Some(Some(a)))
      })
    )
  }
}

Now I am converting my play-json code to Circe, but I can't figure out how to write a Decoder[Option[Option[A]] that has the same behavior. That is, I need

def optOptDecoder[A](implicit d: Decoder[A]): Decoder[Option[Option[A]] = ??? //help!

Any ideas on how I can make this work? Thanks

I figured this out:

There were two problems:

1) How to deal with the case where the field was completely missing from the JSON. Turns out you have to use Decoder.reattempt in your custom decoder, following Circe's decodeOption code, which works.

2) How to have the compiler recognize cases of Option[Option[A]] when your decoder code is sitting in a helper object (or wherever). Turns out if you're using semi-auto derivation, you can create an implicit in the companion object and that will override the defaults:

//companion object
object MyModel {
  implicit def myModelOptOptDecoder[A](implicit d: Decoder[A]): Decoder[Option[Option[A]]] = 
    MyHelperObject.optOptDecoder
  implicit val myModelDecoder: Decoder[MyModel] = deriveDecoder
}

Anyway, I don't think this will be much help to anybody in the future, so unless I get any upvotes in the next few hours I think I'll just delete this.

Edit2: Okay it was answered so I won't delete it. Stay strong, esoteric circe question, stay strong...

Upvotes: 2

Views: 1356

Answers (2)

Devon Sams
Devon Sams

Reputation: 1414

Here is another solution I found (This is not my gist):

sealed trait UpdateOrDelete[+A]

case object Delete                                    extends UpdateOrDelete[Nothing]
final case class UpdateOptionalFieldWith[A](value: A) extends UpdateOrDelete[A]

object UpdateOrDelete {
  implicit def optionalDecoder[A](implicit decodeA: Decoder[A]): Decoder[UpdateOptionalField[A]] =
    Decoder.withReattempt {
      // We're trying to decode a field but it's missing.
      case c: FailedCursor if !c.incorrectFocus => Right(None)
      case c =>
        Decoder.decodeOption[A].tryDecode(c).map {
          case Some(a) => Some(UpdateOptionalFieldWith(a))
          case None    => Some(Delete)
        }
    }

  // Random UUID to _definitely_ avoid collisions
  private[this] val marker: String   = s"$$marker-${UUID.randomUUID()}-marker$$"
  private[this] val markerJson: Json = Json.fromString(marker)

  implicit def optionalEncoder[A](implicit encodeA: Encoder[A]): Encoder[UpdateOptionalField[A]] =
    Encoder.instance {
      case Some(Delete)                     => Json.Null
      case Some(UpdateOptionalFieldWith(a)) => encodeA(a)
      case None                             => markerJson
    }

  def filterMarkers[A](encoder: Encoder.AsObject[A]): Encoder.AsObject[A] =
    encoder.mapJsonObject { obj =>
      obj.filter {
        case (_, value) => value =!= markerJson
      }
    }
}

Upvotes: 1

Joe K
Joe K

Reputation: 18424

An Option[Option[A]] is a bit odd. I understand and mostly agree with the reasoning, but I think it's weird enough that it may warrant just replacing it with your own class (and writing a decoder for that). Something like:

sealed trait OptionalNull[+A] {
  def toOption: Option[Option[A]]
}
object NotPresent extends OptionalNull[Nothing] {
  override def toOption = None
}
object PresentButNull extends OptionalNull[Nothing] {
  override def toOption = Some(None)
}
case class PresentNotNull[A](value: A) extends OptionalNull[A] {
  override def toOption = Some(Some(value))
}

This has the additional benefit of not having to worry about implicit precedence and stuff like that. Might simplify your decoder.

Upvotes: 2

Related Questions