【问题标题】:Scala circe deriveUnwrapped value class doesn't work for missing memberScala circe derivedUnwrapped 值类不适用于缺少的成员
【发布时间】:2021-01-27 16:35:34
【问题描述】:

我正在尝试解码一个字符串值类,如果字符串为空,我需要得到一个无,否则是一些。我有以下菊石脚本示例:

import $ivy.`io.circe::circe-generic:0.13.0`, io.circe._, io.circe.generic.auto._, io.circe.syntax._, io.circe.generic.JsonCodec
import $ivy.`io.circe::circe-generic-extras:0.13.0`, io.circe.generic.extras._, io.circe.generic.extras.semiauto._
import $ivy.`io.circe::circe-parser:0.13.0`, io.circe.parser._

final case class CustomString(value: Option[String]) extends AnyVal
final case class TestString(name: CustomString)

implicit val customStringDecoder: Decoder[CustomString] =
    deriveUnwrappedDecoder[CustomString].map(ss => CustomString(ss.value.flatMap(s => Option.when(s.nonEmpty)(s))))

implicit val customStringEncoder: Encoder[CustomString] = deriveUnwrappedEncoder[CustomString]
implicit val testStringCodec: Codec[TestString] = io.circe.generic.semiauto.deriveCodec

val testString = TestString(CustomString(Some("test")))
val emptyTestString = TestString(CustomString(Some("")))
val noneTestString = TestString(CustomString(None))
val nullJson = """{"name":null}"""
val emptyJson = """{}"""

assert(testString.asJson.noSpaces == """{"name":"test"}""")
assert(emptyTestString.asJson.noSpaces == """{"name":""}""")
assert(noneTestString.asJson.noSpaces == nullJson)
assert(noneTestString.asJson.dropNullValues.noSpaces == emptyJson)

assert(decode[TestString](nullJson).exists(_ == noneTestString)) // this passes
assert(decode[TestString](emptyJson).exists(_ == noneTestString)) // this fails

【问题讨论】:

    标签: scala circe refined


    【解决方案1】:

    据我所知,没有自动功能。

    我会直接使用circe cursor api来解决:

    import $ivy.`io.circe::circe-generic:0.13.0`, io.circe._, io.circe.generic.auto._, io.circe.syntax._, io.circe.generic.JsonCodec
    import $ivy.`io.circe::circe-generic-extras:0.13.0`, io.circe.generic.extras._, io.circe.generic.extras.semiauto._
    import $ivy.`io.circe::circe-parser:0.13.0`, io.circe.parser._
    
    final case class CustomString(value: Option[String]) extends AnyVal
    final case class TestString(name: CustomString)
    
    implicit val testStringDecoder: Decoder[TestString] =  (c: HCursor) =>{
         c.downField("name").as[Option[String]].map(string => TestString(CustomString(string)))
    }
    
    implicit val customStringEncoder: Encoder[CustomString] = deriveUnwrappedEncoder[CustomString]
    implicit val testStringCodec: Encoder[TestString] = io.circe.generic.semiauto.deriveEncoder
    
    val testString = TestString(CustomString(Some("test")))
    val emptyTestString = TestString(CustomString(Some("")))
    val noneTestString = TestString(CustomString(None))
    val nullJson = """{"name":null}"""
    val emptyJson = """{}"""
    
    assert(testString.asJson.noSpaces == """{"name":"test"}""")
    assert(emptyTestString.asJson.noSpaces == """{"name":""}""")
    assert(noneTestString.asJson.noSpaces == nullJson)
    assert(noneTestString.asJson.dropNullValues.noSpaces == emptyJson)
    
    assert(decode[TestString](nullJson).exists(_ == noneTestString)) // this passes
    assert(decode[TestString](emptyJson).exists(_ == noneTestString)) // this fails
    

    【讨论】:

    • 尽管这不是我一直在寻找的答案,但它为我指明了正确的方向。这是我达到的解决方案:``` Decoder .decodeOption(deriveUnwrappedDecoder[CustomString].map(ss => CustomString(ss.value.flatMap(s => Option.when(s.nonEmpty)(s))))) .map(csOpt => CustomString(csOpt.flatMap(_.value))) ```
    • 清理了一下``` 解码器 .decodeOption(deriveUnwrappedDecoder[CustomString]) .map(ssOpt => CustomString(ssOpt.flatMap(_.value.flatMap(s => Option.when( s.nonEmpty)(s))))) ```
    【解决方案2】:

    您可以交替使用不同的编码,这样意图更加清晰,并且当您需要使用字符串时,您不需要在嵌套的 case 类上进行模式匹配。

    final case class TestString(name: Option[NonEmptyString])
    object TestString {
      implicit val decoder: Decoder[TestString] = deriveDecoder
    }
    
    sealed trait NonEmptyString {
      def value: String
    }
    object NonEmptyString {
      private case class NonEmptyStringImpl(value: String) extends NonEmptyString
    
      def apply(value: String): Either[NonEmptyStringRequiredException, NonEmptyString] = {
        if (value.nonEmpty) Right(NonEmptyStringImpl(value))
        else Left(new NonEmptyStringRequiredException)
      }
    
      implicit val encoder: Encoder[NonEmptyString] = Encoder[String].contramap(_.value)
    
      implicit val decoder: Decoder[Option[NonEmptyString]] = Decoder.withReattempt {
        case h: HCursor =>
          if (h.value.isNull) Right(None)
          else h.value.asString match {
            case Some(string) => Right(apply(string).toOption)
            case None => Left(DecodingFailure("Not a string.", h.history))
          }
        case _: FailedCursor =>
          Right(None)
      }
    }
    

    【讨论】:

    • 刚刚意识到circerefined 之间存在集成,我可以通过在我的构建中添加"io.circe" % "circe-refined_2.13" % "0.13.0" 来直接使用Option[NonEmptyString]
    【解决方案3】:

    现有的答案并不能解决问题,所以这里是解决方案。如果不想使用refined,可以这样定义解码器:

    implicit val customStringDecoder: Decoder[CustomString] =
      Decoder
        .decodeOption(deriveUnwrappedDecoder[CustomString])
        .map(ssOpt => CustomString(ssOpt.flatMap(_.value.flatMap(s => Option.when(s.nonEmpty)(s)))))
    

    但是,如果您使用精炼类型(我推荐),使用 circe-refined 可以更简单,并且它具有更好的类型安全性(即您知道您的 String 不是空的)。这是用于测试的完整菊石脚本:

    import $ivy.`io.circe::circe-generic:0.13.0`, io.circe._, io.circe.generic.auto._, io.circe.syntax._
    import $ivy.`io.circe::circe-parser:0.13.0`, io.circe.parser._
    
    import $ivy.`eu.timepit::refined:0.9.14`, eu.timepit.refined.types.string.NonEmptyString
    import $ivy.`io.circe::circe-refined:0.13.0`, io.circe.refined._
    
    final case class TestString(name: Option[NonEmptyString])
    
    implicit val customNonEmptyStringDecoder: Decoder[Option[NonEmptyString]] =
        Decoder[Option[String]].map(_.flatMap(NonEmptyString.unapply))
    
    val testString = TestString(NonEmptyString.unapply("test"))
    val emptyTestString = TestString(NonEmptyString.unapply(""))
    val noneTestString = TestString(None)
    val nullJson = """{"name":null}"""
    val emptyJson = """{}"""
    val emptyStringJson = """{"name":""}"""
    
    assert(testString.asJson.noSpaces == """{"name":"test"}""")
    assert(noneTestString.asJson.noSpaces == nullJson)
    assert(noneTestString.asJson.dropNullValues.noSpaces == emptyJson)
    
    
    assert(decode[TestString](nullJson).exists(_ == noneTestString))
    assert(decode[TestString](emptyJson).exists(_ == noneTestString))
    assert(decode[TestString](emptyStringJson).exists(_ == noneTestString))
    

    【讨论】:

      猜你喜欢
      • 2017-10-27
      • 2015-08-07
      • 2019-05-31
      • 1970-01-01
      • 2020-06-07
      • 1970-01-01
      • 1970-01-01
      • 2017-06-23
      • 2020-08-04
      相关资源
      最近更新 更多