如何使用 Circe 的手动解码器反序列化一个非固定的 json 数组?

How can I deserialize an non-fixed array of jsons using Circe's manual decoder?

我有一个 JSON 看起来像:

{
  "data": [
    {
      "id": "1",
      "email": "hello@world.com",
      "name": "Mr foo",
      "roles": [
        "Chief Bar Officer"
      ],
      "avatar_url": null,
      "phone_number": null
    },
    {
      "id": "2",
      "email": "bye@world.com",
      "name": "Mr baz",
      "roles": [
        "Chief Baz Officer"
      ],
      "avatar_url": null,
      "phone_number": null
    }
  ]
}

我主要对 parsing/deserializing 数据列表感兴趣,我想手动完成(出于某种神秘原因,我更喜欢手动方式)。

如果这是相关的,我正在使用 sttp 的 circe 库 sttp.client.circe._,目的是使用 asJson.[=17= 将来自 get 请求的传入数据直接解析为 Json ]

获取 sttp 请求类似于:

val r1 = basicRequest
    .get(uri"https://woooo.woo.wo/v1/users")
    .header("accept", "application/json")
    .header("Authorization", "topsecret"
    .response(asJson[SomeClass])

这是我目前尝试过的方法:

// Define the case class
case class User(
    id: String,
    email: String,
    name: String,
    roles: List[String],
    avatar_url: Option[String],
    phone_number: Option[String]
)

// Define the manual deserializer

case object User {

  implicit val userDecoder: Decoder[User] = (hCursor: HCursor) => {
    val data = hCursor.downField("data").downArray
    for {
      id <- data.get[String]("id")
      email <- data.get[String]("email")
      name <- data.get[String]("name")
      roles <- data.get[List[String]]("roles")
      avatarUrl <- data.get[Option[String]]("avatarUrl")
      phoneNumber <- data.get[Option[String]]("phoneNumber")
    } yield User(id, email, name, roles, avatarUrl, phoneNumber)
  }
}

我的方法的问题(我认为)是 .downArray 让我只序列化用户数组中的第一个用户。

我的 objective 是为了能够拥有一些用户序列(可能类似于 List[User]),但目前我最终只反序列化了数组中的一个用户。

值得一提的是,“数据”数组不包含固定数量的用户,每次 api 调用都可能导致不同数量的用户。

感谢 Travis Brown and the circe Gitter community 帮助我解决了这个问题。

我在这里引用特拉维斯的话:

it would be better to build up the instance you need to parse the top-level JSON object compositionally… i.e. have a Decoder[User] that only decodes a single user JSON object, and then use Decoder[List[User]].at("data") or something similar to decode the top-level JSON object containing the data field with the JSON array.

我最终得到了一个看起来像这样的实现:

case class Users(users: List[User])

case object User {

  implicit val usrDecoder: Decoder[User] = (hCursor: HCursor) => {

    for {
      id <- hCursor.get[String]("id")
      email <- hCursor.get[String]("email")
      name <- hCursor.get[String]("name")
      roles <- hCursor.get[List[String]]("roles")
      avatarUrl <- hCursor.get[Option[String]]("avatarUrl")
      phoneNumber <- hCursor.get[Option[String]]("phoneNumber")
    } yield User(id, email, name, roles, avatarUrl, phoneNumber)
  }

  implicit val decodeUsers: Decoder[Users] =
    Decoder[List[User]].at("data").map(Users)

}

想法是分别组成一个用户的解码器和一个 collection 个用户的解码器。然后通过将 Users 映射到 Decoder,我们将 Decoder 的结果包装到 Users case class.

如果您想解码 List[User],您需要为确切类型 List[User] 创建解码器。它可能看起来像:

implicit val userDecoder: Decoder[List[User]] = (hCursor: HCursor) => {
    Either.fromOption(
      hCursor.downField("data").values,
      DecodingFailure("Can't decode data", Nil)
    ).flatMap { values =>
      values.toList.map(_.hcursor).traverse {
        userCursor =>
          for {
            id <- userCursor.get[String]("id")
            email <- userCursor.get[String]("email")
            name <- userCursor.get[String]("name")
            roles <- userCursor.get[List[String]]("roles")
            avatarUrl <- userCursor.get[Option[String]]("avatarUrl")
            phoneNumber <- userCursor.get[Option[String]]("phoneNumber")
          } yield User(id, email, name, roles, avatarUrl, phoneNumber)
      }
    }
  }

然后你可以像这样使用它:

json.as[List[User]]

这可能不是最好的主意,因为 circe 已经可以将 json 数组解码为列表,所以最好只为用户(解码对象)创建解码器:

implicit val userDecoder: Decoder[User] = (hCursor: HCursor) =>
  for {
    id <- hCursor.get[String]("id")
    email <- hCursor.get[String]("email")
    name <- hCursor.get[String]("name")
    roles <- hCursor.get[List[String]]("roles")
    avatarUrl <- hCursor.get[Option[String]]("avatarUrl")
    phoneNumber <- hCursor.get[Option[String]]("phoneNumber")
} yield User(id, email, name, roles, avatarUrl, phoneNumber)

在这种情况下,您需要手动进入 data 字段:

json.hcursor.downField("data").as[List[User]]

另一种可能性是为 data 包装器创建 case class:

  case class Data[D](data: D)

  object Data {
    implicit def dataDecoder[D: Decoder]: Decoder[Data[D]] =
      (hCursor: HCursor) => hCursor.get[D]("data").map(Data(_))
  }

然后你可以这样做:

json.as[Data[List[User]]]