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!

2 Answers 2

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)
}
Sign up to request clarification or add additional context in comments.

2 Comments

Thanks for your answer, a little ugly indeed but I think I can make it work :)
@filaton I am glad it worked for you. What should I do when someone answers my question?
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)
}

1 Comment

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 Structs InfoPlayers and InfoGames, each ignoring the other`s field, but maybe there is a "nicer" option. Any opinion?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.