Класс значений Scala circe, производных Unwrapped, не работает для отсутствующего члена

Я пытаюсь декодировать класс значений String, в котором, если строка пуста, мне нужно получить None, иначе Some. У меня есть следующий пример сценария аммонита:

      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

3 ответа

Вы можете альтернативно использовать другую кодировку, чтобы цель была более ясной, и вам не нужно было бы сопоставлять шаблоны во вложенном классе 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)
  }
}

Насколько я знаю, для этого нет автоматической функции.

Я бы решил это, используя API курсора circe напрямую:

      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

Существующие ответы не решают проблему, поэтому вот решение. Если вы не хотите использовать Fine , вы можете определить декодер следующим образом:

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

Однако, если вы используете уточненные типы (которые я рекомендую), это может быть еще проще с помощью circe-refinedи это обеспечивает лучшую безопасность типов (т.е. вы знаете, что ваша строка не пуста). Вот полный скрипт аммонита для тестирования:

      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))
Другие вопросы по тегам