Как я могу десериализовать нефиксированный массив jsons с помощью ручного декодера Circe? - PullRequest
2 голосов
/ 06 августа 2020

У меня 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
    }
  ]
}

Меня в основном интересует синтаксический анализ / десериализация списка данных, и я хотел бы сделать это вручную (для некоторых я предпочитаю ручной способ таинственная причина).

В случае, если это актуально, я использую sttp-библиотеку circe sttp.client.circe._ с намерением анализировать входящие данные из запросов на получение непосредственно в Json, используя asJson.

Запрос на получение 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 заставляет меня сериализовать только первого пользователя в массиве Users.

Моя цель - иметь возможность иметь некоторую последовательность пользователей (что-то вроде List[User] возможно), но на данный момент я завершаю десериализацию только одного user в массиве.

Стоит отметить, что массив «data» не содержит фиксированного числа пользователей, и каждый вызов API может привести к другому количеству пользователей.

Ответы [ 2 ]

3 голосов
/ 06 августа 2020

Спасибо Трэвису Брауну и сообществу Circe Gitter за помощь мне в этом разобраться.

Я цитирую Трэвиса:

было бы лучше создать экземпляр, который вам нужен для композиционного анализа объекта верхнего уровня JSON ... т.е. иметь Decoder [User], который декодирует только один пользовательский объект JSON, а затем используйте Decoder [List [User]]. at ("data") или что-то подобное для декодирования объекта верхнего уровня JSON, содержащего поле данных с массивом JSON.

У меня есть В итоге получилась реализация, которая выглядит примерно так:

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)

}

Идея состоит в том, чтобы скомпоновать декодер пользователя и декодер для набора пользователей отдельно. Затем, сопоставив Users с декодером, мы переносим результаты декодера в класс случая Users.

1 голос
/ 06 августа 2020

Если вы хотите декодировать 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)

В этом случае вы должны вручную go вниз data поле:

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

Еще одна возможность - просто создать класс case для data оболочки:

  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]]]
...