Класс значений 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))