scalascalazfinchcirce

Circe decoder for scalaz.Maybe


Here's a simple finch server, using circe as decoder:

import com.twitter.finagle.http.RequestBuilder
import com.twitter.io.Buf
import io.circe.generic.auto._
import io.finch._
import io.finch.circe._

case class Test(myValue: Int)

val api = post("foo" :: body.as[Test]) { test: Test => Ok(test) }

val bodyPost = RequestBuilder()
  .url("http://localhost:8080/foo")
  .buildPost(Buf.Utf8("""{ "myValue" : 42 }"""))

api.toService.apply(bodyPost).onSuccess { response =>
  println(s"$response: ${response.contentString}")
}

// output: Response("HTTP/1.1 Status(200)"): {"myValue":42}

Changing the myValue into an Option works out of the box, giving the same result as above code. However, changing it into a scalaz.Maybe:

import scalaz.Maybe
case class Test(myValue: Maybe[Int])

results in:

Response("HTTP/1.1 Status(400)"): {"message":"body cannot be converted to Test: CNil: El(DownField(myValue),true,false)."}

How should I implement the needed encoder/decoder?


Solution

  • Here's a slightly different approach:

    import io.circe.{ Decoder, Encoder }
    import scalaz.Maybe
    
    trait ScalazInstances {
      implicit def decodeMaybe[A: Decoder]: Decoder[Maybe[A]] =
        Decoder[Option[A]].map(Maybe.fromOption)
    
      implicit def encodeMaybe[A: Encoder]: Encoder[Maybe[A]] =
        Encoder[Option[A]].contramap(_.toOption)
    }
    
    object ScalazInstances extends ScalazInstances
    

    And then:

    scala> import scalaz.Scalaz._, ScalazInstances._
    import scalaz.Scalaz._
    import ScalazInstances._
    
    scala> import io.circe.parser.decode, io.circe.syntax._
    import io.circe.parser.decode
    import io.circe.syntax._
    
    scala> Map("a" -> 1).just.asJson.noSpaces
    res0: String = {"a":1}
    
    scala> decode[Maybe[Int]]("1")
    res1: Either[io.circe.Error,scalaz.Maybe[Int]] = Right(Just(1))
    

    The main advantage of this implementation (apart from the fact that it's more generic and even a little more concise) is that it has the behavior you generally expect for optional members in case classes. With your implementation, for example, the following inputs fail:

    scala> import io.circe.generic.auto._
    import io.circe.generic.auto._
    
    scala> case class Foo(i: Maybe[Int], s: String)
    defined class Foo
    
    scala> decode[Foo]("""{ "s": "abcd" }""")
    res2: Either[io.circe.Error,Foo] = Left(DecodingFailure(Attempt to decode value on failed cursor, List(DownField(i))))
    
    scala> decode[Foo]("""{ "i": null, "s": "abcd" }""")
    res3: Either[io.circe.Error,Foo] = Left(DecodingFailure(Int, List(DownField(i))))
    

    While if you use the decoder above that just delegates to the Option decoder, they get decoded to Empty:

    scala> decode[Foo]("""{ "s": "abcd" }""")
    res0: Either[io.circe.Error,Foo] = Right(Foo(Empty(),abcd))
    
    scala> decode[Foo]("""{ "i": null, "s": "abcd" }""")
    res1: Either[io.circe.Error,Foo] = Right(Foo(Empty(),abcd))
    

    Whether you want this behavior or not is up to you, of course, but it's what most people are likely to expect from a Maybe codec.

    Footnote

    One disadvantage (in some very specific cases) of my decoder is that it instantiates an extra Option for every successfully decoded value. If you're extremely concerned about allocations (or if you're just curious about how this stuff works, which is probably a better reason), you can implement your own based on circe's decodeOption:

    import cats.syntax.either._
    import io.circe.{ Decoder, DecodingFailure, Encoder, FailedCursor, HCursor }
    import scalaz.Maybe
    
    implicit def decodeMaybe[A](implicit decodeA: Decoder[A]): Decoder[Maybe[A]] =
      Decoder.withReattempt {
        case c: HCursor if c.value.isNull => Right(Maybe.empty)
        case c: HCursor => decodeA(c).map(Maybe.just)
        case c: FailedCursor if !c.incorrectFocus => Right(Maybe.empty)
        case c: FailedCursor => Left(DecodingFailure("[A]Maybe[A]", c.history))
      }
    

    The Decoder.withReattempt part is the magic that allows us to decode something like {} into a case class Foo(v: Maybe[Int]) and get Foo(Maybe.empty) as expected. The name is a little confusing, but what it really means is "apply this decoding operation even if the last operation failed". In the context of parsing e.g. a case class like case class Foo(v: Maybe[Int]), the last operation would be the attempt to select a "v" field in the JSON object. If there's no "v" key, normally that would be the end of the story—our decoder wouldn't even be applied, because there's nothing to apply it to. withReattempt allows us to continue decoding anyway.

    This code is pretty low-level and these parts of the Decoder and HCursor APIs are designed more for efficiency than for user-friendliness, but it's still possible to tell what's going on if you stare at it. If the last operation didn't fail, we can check whether the current JSON value is null and return Maybe.empty if it is. If it's not, we try to decode it as an A and wrap the result in Maybe.just if it succeeds. If the last operation failed, we first check whether the operation and the last focus are mismatched (a detail that's necessary because of some weird corner cases—see my proposal here and the linked bug report for details). If they're not, we succeed emptily. If they are mismatched, we fail.

    Again, you almost certainly shouldn't use this version—mapping over the Decoder[Option[A]] is clearer, more future-proof, and only very slightly less efficient. Understanding withReattempt can be useful anyway, though.