使用 Scala 表示两个 JSON 字段,其中只有一个可以为 null

Using Scala to represent two JSON fields of which only one can be null

假设我的 API returns 看起来像这样的 JSON:

{
  "field1": "hey",
  "field2": null,
}

我有这个规则,这些字段中只有一个可以同时是null。在这个例子中,只有 field2null 所以我们没问题。

我可以用以下情况在 Scala 中表示这一点 class:

case class MyFields(
  field1: Option[String],
  field2: Option[String]
)

并且通过实现一些隐式并让 circe 完成将对象转换为 JSON 的魔法。

object MyFields {
  implicit lazy val encoder: Encoder[MyFields] = deriveEncoder[MyFields]
  implicit lazy val decoder: Decoder[MyFields] = deriveDecoder[MyFields]

现在,这个策略奏效了。有点。

MyFields(Some("hey"), None)
MyFields(None, Some("hey"))
MyFields(Some("hey"), Some("hey"))

这些都导致 JSON 遵循 规则 。但也可以这样做:

MyFields(None, None)

这将导致 JSON 打破 规则

所以这个策略没有充分表达规则。什么是更好的方法?

这是应用程序级数据验证示例,以及范围检查值和其他一致性检查。因此,它并不真正属于原始 JSON 解析,而是需要在单独的验证步骤中完成。

所以我建议有一组 类 直接表示 JSON 数据,然后一组单独的应用程序 类 使用应用程序数据类型而不是 JSON 数据类型。

读取数据时,将JSON读入数据类,检查底层JSON是否有效。然后这些数据 类 通过适当的验证和转换(检查值范围、将字符串更改为枚举等)转换为应用程序 类

这允许在不影响应用程序逻辑的情况下更改数据格式(例如 XML 或数据库)。

将您的数据表示为具有成员 val field1or2 = Either[String, String]。如果 circe 内置 Codec.codecForEither 不能满足您的确切要求,您可以手动编写编解码器。根据您的描述(field1 和 fiel2 都必须出现在 json 中,一个作为字符串,一个作为 null),类似于

import io.circe.{Decoder, Encoder, HCursor, Json, DecodingFailure}

case class Fields(fields: Either[String, String])

implicit val encodeFields: Encoder[Fields] = new Encoder[Fields] {
  final def apply(a: Fields): Json = Json.obj(a.fields match {
    case Left(str) => {
      "field1" -> Json.fromString(str)
      "field2" -> Json.Null
    }
    case Right(str) => {
      "field1" -> Json.Null
      "field2" -> Json.fromString(str)
    }
  })
}

implicit val decodeFields: Decoder[Fields] = new Decoder[Fields] {
  final def apply(c: HCursor): Decoder.Result[Fields] ={
    val f1 = c.downField("field1").as[Option[String]]
    val f2 = c.downField("field1").as[Option[String]]
    (f1, f2) match {
      case (Right(None), Right(Some(v2))) => Right(Fields(Right(v2)))
      case (Right(Some(v1)), Right(None)) => Right(Fields(Left(v1)))
      case (Left(failure), _) => Left(failure)
      case (_, Left(failure)) => Left(failure)
      case (Right(None), Right(None)) => Left(DecodingFailure("Either field1 or field2 must be non-null", Nil))
      case (Right(Some(_)), Right(Some(_))) => Left(DecodingFailure("field1 and field2 may not both be non-null", Nil))
    }
  }
}

(这是基于 and 。)

可以使用

Cats Ior datatype,如下:

import cats.data.Ior
import io.circe.parser._
import io.circe.syntax._
import io.circe._


case class Fields(fields: Ior[String, String])

implicit val encodeFields: Encoder[Fields] = (a: Fields) =>
  a.fields match {
    case Ior.Both(v1, v2) => Json.obj(
      ("field1", Json.fromString(v1)),
      ("field2", Json.fromString(v2))
    )
    case Ior.Left(v) => Json.obj(
      ("field1", Json.fromString(v)),
      ("field2", Json.Null)
      )
    case Ior.Right(v) => Json.obj(
      ("field1", Json.Null),
      ("field2", Json.fromString(v))
    )
  }

implicit val decodeFields: Decoder[Fields] = (c: HCursor) => {
  val f1 = c.downField("field1").as[Option[String]]
  val f2 = c.downField("field2").as[Option[String]]
  (f1, f2) match {
    case (Right(Some(v1)), Right(Some(v2))) => Right(Fields(Ior.Both(v1, v2)))
    case (Right(Some(v1)), Right(None)) => Right(Fields(Ior.Left(v1)))
    case (Right(None), Right(Some(v2))) => Right(Fields(Ior.Right(v2)))
    case (Left(failure), _) => Left(failure)
    case (_, Left(failure)) => Left(failure)
    case (Right(None), Right(None)) => Left(DecodingFailure("At least one of field1 or field2 must be non-null", Nil))
  }
}



println(Fields(Ior.Right("right")).asJson)
println(Fields(Ior.Left("left")).asJson)
println(Fields(Ior.both("right", "left")).asJson)


println(parse("""{"field1": null, "field2": "right"}""").flatMap(_.as[Fields]))