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?
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.
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.