Как я могу десериализовать нефиксированный массив jsons с помощью ручного декодера Circe?
У меня есть 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
заставляет меня сериализовать только первого пользователя в массиве пользователей.
Моя цель - иметь некоторую последовательность пользователей (что-то вроде List[User]
возможно), но на данный момент я завершаю десериализацию только одного пользователя в массиве.
Стоит упомянуть, что массив "данных" не содержит фиксированного числа пользователей, и каждый вызов API может привести к другому количеству пользователей.
2 ответа
Спасибо за помощь Travis Brown и сообществу 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
в Decoder мы переносим результаты Decoder в класс случая Users.
Если вы хотите расшифровать 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]]
Еще одна возможность - просто создать класс 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]]]