具有相关类型的Swift JSON解码

问题描述

我正在尝试在Swift中解码“相关的” JSON API响应。让我们想象一个具有两个端点的虚构API:

  • /players,返回具有以下属性的对象数组:
    • id,代表玩家ID的整数
    • name,代表玩家名称的字符串
  • /games,返回具有以下属性的对象数组:
    • name,代表游戏名称的字符串
    • playerId1,代表第一个玩家ID的整数
    • playerId2,代表第二个玩家ID的整数

我使用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"
    }
}

我想将/games的响应解码为具有正确的Game属性Player对象数组,因此我使用了自定义初始化程序扩展了Game,但我不知道如何检索所有玩家属性

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

总而言之,来自/games的API响应未包含我进行完全初始化所需的所有信息,因此我应该如何进行:

  • 我能否/应该进行两个API调用一个/games,另一个players,并以某种方式在解码之前合并它们?
  • 我是否应该仅部分初始化Player(将未知内容留给nil)并稍后填充详细信息? (听起来很危险而且麻烦。)
  • 还有什么?

如果要尝试使用它,可以找到完整的示例here

解决方法

我的建议是添加两个惰性的实例化属性,以从数组中获取Player实例。

惰性属性优于计算属性的好处是,该值仅计算一次,直到第一次访问该值。并且不需要自定义init(from:)方法。

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 }
   
}

或者创建一个CodingUserInfoKey

extension CodingUserInfoKey {
    static let players = CodingUserInfoKey(rawValue: "players")!
}

和扩展名JSONDecoder

extension JSONDecoder {
    convenience init(players: [Player]) {
        self.init()
        self.userInfo[.players] = players
    }
}

并在JSON解码器的players对象中传递userInfo数组

let decoder = JSONDecoder(players: players)
let games = try! decoder.decode([Game].self,from: Data(gamesResponse.utf8))
dump(games[0].player1)

现在您可以通过init(from:方法获得实际的玩家了。

struct Game: Decodable {
    let name: String
    let player1: Player
    let player2: Player
    
    enum CodingKeys: String,CodingKey {
        case name,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 }!
    }
}

代码假定players数组包含与Player值相对应的所有playerId实例。如果不是,则必须声明player1player2为可选,并删除感叹号。