【问题标题】:Circe decoder for scalaz.Maybescalaz.Maybe的Circe解码器
【发布时间】:2017-02-13 16:45:22
【问题描述】:

这是一个简单的 finch 服务器,使用 circe 作为解码器:

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}

myValue 更改为Option 开箱即用,结果与上述代码相同。但是,将其更改为scalaz.Maybe

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

结果:

Response("HTTP/1.1 Status(400)"): {"message":"body 无法转换 测试:CNil: El(DownField(myValue),true,false)。"}

我应该如何实现所需的编码器/解码器?

【问题讨论】:

    标签: scala scalaz finch circe


    【解决方案1】:

    这里有一个稍微不同的方法:

    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
    

    然后:

    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))
    

    这种实现的主要优点(除了它更通用甚至更简洁一点之外)是它具有您通常期望的 case 类中的可选成员的行为。例如,在您的实施中,以下输入失败:

    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))))
    

    如果您使用上面仅代表Option 解码器的解码器,它们会被解码为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))
    

    当然,您是否想要这种行为取决于您,但这是大多数人对 Maybe 编解码器的期望。

    脚注

    我的解码器的一个缺点(在某些非常特殊的情况下)是它为每个成功解码的值实例化一个额外的Option。如果您非常关心分配(或者如果您只是对这些东西的工作原理感到好奇,这可能是一个更好的理由),您可以根据 circe 的 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))
      }
    

    Decoder.withReattempt 部分是一种魔力,它允许我们将{} 之类的内容解码为case class Foo(v: Maybe[Int]) 并按预期获得Foo(Maybe.empty)。这个名字有点混乱,但真正的意思是“即使最后一次操作失败,也要应用这个解码操作”。在解析的上下文中,例如像case class Foo(v: Maybe[Int]) 这样的案例类,最后一个操作是尝试在 JSON 对象中选择 "v" 字段。如果没有"v" 键,通常这将是故事的结尾——我们的解码器甚至不会被应用,因为没有任何东西可以应用它。 withReattempt 允许我们继续解码。

    这段代码非常低级,DecoderHCursor API 的这些部分更多地是为了提高效率而不是为了用户友好,但如果你盯着它看,仍然可以知道发生了什么。如果最后一个操作没有失败,我们可以检查当前JSON值是否为null,如果是则返回Maybe.empty。如果不是,我们尝试将其解码为A,如果成功则将结果包装在Maybe.just 中。如果最后一个操作失败,我们首先检查操作和最后一个焦点是否不匹配(由于一些奇怪的极端情况,这个细节是必要的——请参阅我的建议here 和链接的bug report 了解详细信息)。如果他们不是,我们就空虚地成功。如果它们不匹配,我们就会失败。

    同样,您几乎可以肯定不应该使用这个版本——Decoder[Option[A]] 上的映射更清晰、更面向未来,而且效率稍低。不过,了解withReattempt 还是很有用的。

    【讨论】:

    • 所以基本上你说的是“不要自己做整个事情,因为里面有漏洞,最好委托给现有的 Option 编解码器来处理缺失值和空值等问题正确”。我当然同意。但是,如果它是与circe.generic.auto._ 中已经存在的东西不同构的其他类型,那么像我的答案中的那种手动方法仍然可以吗? (只要我设法修补大部分漏洞)
    【解决方案2】:

    这是一个可能的实现:

    implicit def encodeDecodeMaybe: Encoder[Maybe[Int]] with Decoder[Maybe[Int]] = new Encoder[Maybe[Int]] with Decoder[Maybe[Int]] {
        override def apply(a: Maybe[Int]): Json = Encoder.encodeInt.apply(a.getOrElse(0)) // zero if Empty
        override def apply(c: HCursor): Decoder.Result[Maybe[Int]] = Decoder.decodeInt.map(s => Just(s)).apply(c)
    }
    

    【讨论】:

    • 有一个赞成票,但也请参阅我的回答了解一些细节。 :)
    • @Travis Brown 我希望能得到一些细节 :) 非常感谢!
    猜你喜欢
    • 2017-11-27
    • 2017-06-12
    • 2019-01-26
    • 2019-03-15
    • 2019-07-01
    • 1970-01-01
    • 1970-01-01
    • 2017-05-24
    • 2018-10-22
    相关资源
    最近更新 更多