1

Context

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 structs:

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 Players and Games 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)!)

The question

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 Players?

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)!)

What I tried so far

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!

gcharita
  • 7,729
  • 3
  • 20
  • 37
filaton
  • 2,257
  • 17
  • 27

2 Answers2

1

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)
}
gcharita
  • 7,729
  • 3
  • 20
  • 37
  • Thanks for your answer, a little ugly indeed but I think I can make it work :) – filaton Nov 05 '20 at 05:56
  • @filaton I am glad it worked for you. [What should I do when someone answers my question?](https://stackoverflow.com/help/someone-answers) – gcharita Nov 05 '20 at 09:02
0

You just need to provide a root structure and get its data players. No need to decode the values you don't want:


struct ApiResponse: Decodable {
    let data: ApiData
}

struct ApiData: Decodable {
    let players: [Player]
    let games: [Game]
}

struct Player: Codable {
    let id: Int
    let name: String
}

struct Game: Decodable {
    var name: String
    var player1: Int
    var player2: Int
    enum CodingKeys: String, CodingKey {
        case name, player1 = "playerId1", player2 = "playerId2"
    }
}

let common = """
{
    "data":
    {
        "players": [
        {
            "id": 1,
            "name": "John Doe"
        },
        {
            "id": 15,
            "name": "Jessica Thump"
        }],
        "games": [
        {
            "name": "Tic Tac Toe",
            "playerId1": 15,
            "playerId2": 1
        }]
    }
}
"""

do {
    let players = try JSONDecoder().decode(ApiResponse.self, from: Data(common.utf8)).data.players
    print(players)  // [__lldb_expr_48.Player(id: 1, name: "John Doe"), __lldb_expr_48.Player(id: 15, name: "Jessica Thump")]
    let games = try JSONDecoder().decode(ApiResponse.self, from: Data(common.utf8)).data.games
    print(games)  // [__lldb_expr_52.Game(name: "Tic Tac Toe", player1: 15, player2: 1)]
    
    // or get the common data
    let commonData = try JSONDecoder().decode(ApiResponse.self, from: Data(common.utf8)).data
    print(commonData.players)
    print(commonData.games)
} catch {
    print(error)
}
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
  • Thanks for your answer. The issue is that I also need to parse `games`, just that it needs to be done *after* parsing `players`. I could create two `Struct`s `InfoPlayers` and `InfoGames`, each ignoring the other`s field, but maybe there is a "nicer" option. Any opinion? – filaton Nov 04 '20 at 08:09