This is a follow-up question to that question I asked a few days ago, reading it beforehand is not strictly necessary though.
I have an API endpoint /common
, returning JSON data in that form:
{
"data":
{
"players": [
{
"id": 1,
"name": "John Doe"
},
{
"id": 15,
"name": "Jessica Thump"
}],
"games": [
{
"name": "Tic Tac Toe",
"playerId1": 15,
"playerId2": 1
}]
}
}
In further code snippets, it is assumed that this response is stored as a String
in the variable rawApiResponse
.
My aim is to decode that to according Swift struct
s:
struct Player: Decodable {
var id: Int
var name: String?
}
struct Game: Decodable {
var name: String
var player1: Player
var player2: Player
enum CodingKeys: String, CodingKey {
case name
case player1 = "playerId1"
case player2 = "playerId2"
}
}
Thanks to the answer in my original question, I can now decode Player
s and Game
s successfully, but only when the response String
I use is the inner array, e.g.
:
let playersResponse = """
[
{
"id": 1,
"name": "John Doe"
},
{
"id": 15,
"name": "Jessica Thump"
}
]
"""
let players = try! JSONDecoder().decode([Player].self, from: playersResponse.data(using: .utf8)!)
How can I extract only the JSON "players"
array from /common
's API response, so that I can feed it afterwards to a JSON decoder for my Player
s?
Please note that I can't use (or that's at least what I think) the "usual" Decodable
way of making a super-Struct
because I need players
to be decoded before games
(that was the topic of the original question). So, this doesn't work:
struct ApiResponse: Decodable {
let data: ApiData
}
struct ApiData: Decodable {
let players: [Player]
let games: [Game]
}
let data = try! JSONDecoder().decode(ApiResponse.self, from: rawApiResponse.data(using: .utf8)!)
I looked into how to convert a JSON string to a dictionary but that only partially helped:
let json = try JSONSerialization.jsonObject(with: rawApiResponse.data(using: .utf8)!, options: .mutableContainers) as? [String:AnyObject]
let playersRaw = json!["data"]!["players"]!!
If I dump playersRaw
, it looks like what I want, but I have no clue how to cast it to Data
to pass it to my JSONDecoder
, as type(of: playersRaw)
is __NSArrayM
.
I feel like I'm not doing things the way they should be done, so if you have a more "Swifty" solution to the general problem (and not specifically to how to extract a subset of the JSON data), it would be even nicer!
You can make that happen by implementing the decoding yourself in ApiData
and searching for each player id in the players
array:
struct ApiResponse: Decodable {
let data: ApiData
}
struct ApiData: Decodable {
let players: [Player]
var games: [Game]
enum CodingKeys: String, CodingKey {
case players
case games
}
enum GameCodingKeys: String, CodingKey {
case name
case playerId1
case playerId2
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
players = try container.decode([Player].self, forKey: .players)
var gamesContainer = try container.nestedUnkeyedContainer(forKey: .games)
games = []
while !gamesContainer.isAtEnd {
let gameContainer = try gamesContainer.nestedContainer(keyedBy: GameCodingKeys.self)
let playerId1 = try gameContainer.decode(Int.self, forKey: .playerId1)
let playerId2 = try gameContainer.decode(Int.self, forKey: .playerId2)
guard
let player1 = players.first(where: { $0.id == playerId1 }),
let player2 = players.first(where: { $0.id == playerId2 })
else { continue }
let game = Game(
name: try gameContainer.decode(String.self, forKey: .name),
player1: player1,
player2: player2
)
games.append(game)
}
}
}
struct Player: Decodable {
var id: Int
var name: String?
}
struct Game: Decodable {
var name: String
var player1: Player
var player2: Player
}
It's a little ugly, but in the end you can use it like this:
let decoder = JSONDecoder()
do {
let response = try decoder.decode(ApiResponse.self, from: rawApiResponse.data(using: .utf8)!)
let games = response.data.games
print(games)
} catch {
print(error)
}