问题
I'm trying to decode "dependent" JSON API responses in Swift. Let's imagine a fictional API with two endpoints:
/players
, returns an array of objects with following attributes:id
, an integer representing the player IDname
, a string representing the player name
/games
, returns an array of objects with following attributes:name
, a string representing the name of the gameplayerId1
, an integer representing the ID of the first playerplayerId2
, an integer representing the ID of the second player
I model each type with a Swift struct
:
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"
}
}
I want to decode the response from /games
into an array of Game
objects, with correct Player
s attributes, so I extended Game
with a custom initializer but I don't know how to retrieve all player attributes:
extension Game {
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
name = try values.decode(String.self, forKey: .name)
// HOW SHOULD I RETRIEVE THE PLAYER'S NAME GIVEN THEIR ID HERE?
// |
// |
// V
player1 = Player(id: try values.decode(Int.self, forKey: .player1), name: nil)
player2 = Player(id: try values.decode(Int.self, forKey: .player2), name: nil)
}
}
To summarize, the API response from /games
does not contain all the information I need for full initialization, so how should I proceed:
- can/should I make two API calls, one to
/games
and another one toplayers
and somehow merge them before decoding? - should I initialize my
Player
s only partly (leaving unknown stuff tonil
) and fill the details later? (That sound dangerous and cumbersome.) - anything else?
If you want to experiment with it, you can find a full example here
回答1:
My suggestion is to add two lazy instantiated properties to get the Player
instances from the array.
The benefit of a lazy property over a computed property is that the value is calculated once and not until it's being accessed the first time. And a custom init(from:)
method is not needed.
struct Game: Decodable {
let name: String
let playerId1: Int
let playerId2: Int
enum CodingKeys: String, CodingKey { case name, playerId1, playerId2 }
lazy var player1 : Player? = players.first{ $0.id == playerId1 }
lazy var player2 : Player? = players.first{ $0.id == playerId2 }
}
Alternatively create a CodingUserInfoKey
extension CodingUserInfoKey {
static let players = CodingUserInfoKey(rawValue: "players")!
}
and an extension of JSONDecoder
extension JSONDecoder {
convenience init(players: [Player]) {
self.init()
self.userInfo[.players] = players
}
}
and pass the players
array in the userInfo
object of the JSON decoder
let decoder = JSONDecoder(players: players)
let games = try! decoder.decode([Game].self, from: Data(gamesResponse.utf8))
dump(games[0].player1)
Now you can get the actual players in the init(from:
method.
struct Game: Decodable {
let name: String
let player1: Player
let player2: Player
enum CodingKeys: String, CodingKey {
case name, playerId1, playerId2
}
}
extension Game {
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
guard let players = decoder.userInfo[.players] as? [Player] else { fatalError("No players array available") }
name = try values.decode(String.self, forKey: .name)
let playerId1 = try values.decode(Int.self, forKey: .playerId1)
let playerId2 = try values.decode(Int.self, forKey: .playerId2)
player1 = players.first{ $0.id == playerId1 }!
player2 = players.first{ $0.id == playerId2 }!
}
}
The code assumes that the players
array contains all Player
instances corresponding to the playerId
values. If not then you have to declare player1
and player2
as optional and remove the exclamation marks.
来源:https://stackoverflow.com/questions/64636031/swift-json-decoding-with-dependent-types