首页 文章

基于Swift Decodable对象属性中的键的模型关系

提问于
浏览
4

鉴于JSON:

[{
        "name": "TV",
        "room": "Living Room"
    },
    {
        "name": "LightBulb 1",
        "room": "Living Room"
    }
]


struct Room: Decodable {
  let name: String
  let devices: [Device]
}
struct Device: Decodable {
  let name: String
}

如何使用Swift 4 Decodable解码JSON的方式让我的模型结构正确序列化?我想为设备的 room 属性中的每个唯一字符串创建空间,并将这些设备添加到该给定房间的设备列表中 .

一种方法是简单地在没有房间关系的情况下映射它,然后在我获得整个设备列表之后解析该关系,只需运行并按需创建房间,因为我迭代它 . 但这并不像是 The Swift 4™ 这样做的方式 . 有更聪明的方法吗?

2 回答

  • 3

    我在这里做出一个假设 - 通过"the Swift 4 Decodable way of decoding JSON"你的意思是调用 try JSONDecoder().decode([Room].self, from: jsonData) . 如果那个's the case then, to my knowledge, you'运气不好,因为 JSONDecoder 将遍历其解析的JSON对象并在每个对象上调用初始化程序 Room(from: Decoder) . 即使您要创建自己的初始化程序,也无法知道其他JSON对象包含的内容 .

    解决这个问题的一种方法是创建一个反映每个JSON对象属性的中间 Decodable 结构,然后通过遍历这些结构的数组来创建 Room .

    这是一个例子,可以作为Xcode游乐场使用:

    import UIKit
    
    struct Room {
        let name:    String
        var devices: [Device]
    
        fileprivate struct DeviceInRoom: Decodable {
            let name: String
            let room: String
        }
    
        static func rooms(from data: Data) -> [Room]? {
            return (try? JSONDecoder().decode([DeviceInRoom].self, from: data))?.rooms()
        }
    }
    struct Device {
        let name: String
    }
    
    fileprivate extension Array where Element == Room.DeviceInRoom {
        func rooms() -> [Room] {
            var rooms = [Room]()
            self.forEach { deviceInRoom in
                if let index = rooms.index(where: { $0.name == deviceInRoom.room }) {
                    rooms[index].devices.append(Device(name: deviceInRoom.name))
                } else {
                    rooms.append(Room(name: deviceInRoom.room, devices: [Device(name: deviceInRoom.name)]))
                }
            }
            return rooms
        }
    }
    
    let json = """
    [
      {
        "name": "TV",
        "room": "Living Room"
      },
      {
        "name": "LightBulb 1",
        "room": "Living Room"
      }
    ]
    """
    
    if let data  = json.data(using: .utf8),
       let rooms = Room.rooms(from: data) {
    
        print(rooms)
    }
    

    或者 - 也许是更快的Swift4'方式:

    import UIKit
    
    struct Room {
        let name:    String
        var devices: [Device]
    }
    
    struct Device {
        let name: String
    }
    
    struct RoomContainer: Decodable {
    
        let rooms: [Room]
    
        private enum CodingKeys: String, CodingKey {
            case name
            case room
        }
    
        init(from decoder: Decoder) throws {
            var rooms = [Room]()
            var objects = try decoder.unkeyedContainer()
            while objects.isAtEnd == false {
                let container  = try objects.nestedContainer(keyedBy: CodingKeys.self)
                let deviceName = try container.decode(String.self, forKey: .name)
                let roomName   = try container.decode(String.self, forKey: .room)
                if let index = rooms.index(where: { $0.name == roomName }) {
                    rooms[index].devices.append(Device(name: deviceName))
                } else {
                    rooms.append(Room(name: roomName, devices: [Device(name: deviceName)]))
                }
            }
            self.rooms = rooms
        }
    }
    
    let json = """
    [
      {
        "name": "TV",
        "room": "Living Room"
      },
      {
        "name": "LightBulb 1",
        "room": "Living Room"
      }
    ]
    """
    
    if let data  = json.data(using: .utf8),
       let rooms = (try? JSONDecoder().decode(RoomContainer.self, from: data))?.rooms {
    
        print(rooms)
    }
    

    注意 - 我在上面的代码中使用了 try? 几次 . 显然你应该正确处理错误 - JSONDecoder 会根据出错的地方给你很好的具体错误! :)

  • 2

    从一个对象模型映射到另一个对象模型

    和Eivind一起去,因为他已经写了一个很好的答案,我只需加上我的2美分... JSON是一个对象模型,所以我解码JSON对象,然后将这些对象转换为一流的Swift对象 . 只要服务器说JSON,你就必须假设它会在某个时刻发生变化,而你不想要的就是JSON对象模型在Swift世界中流入或指示对象结构甚至是变量名 . 因此,将对象解码为Plain Ol'Swift Objects(POSO)和类型然后进行扩展以处理从这些POSO Decodable到您将构建应用程序的对象的转换是一个非常合理的选择 . 我正在工作的操场位于下方,但是Eivind打败了我,并在他的第二个例子中遇到了生成最终的纯Swift对象的麻烦 .

    Apple关于JSON处理的博客有这个不错的报价

    Working with JSON in Swift

    为了在不同系统之间进行通信,在相同数据的表示之间进行转换对于编写软件来说是一项繁琐的(尽管是必要的)任务 . 因为这些表示的结构可能非常相似,所以创建更高级别的抽象以在这些不同表示之间自动映射可能很诱人 . 例如,类型可以定义snake_case JSON键和camelCase属性名称之间的映射,以便使用Swift反射API(例如Mirror)从JSON自动初始化模型 . 但是,我们发现这些类型的抽象往往不会提供超过Swift语言功能的传统使用的显着优势,而是使调试问题或处理边缘情况变得更加困难 . 在上面的示例中,初始化程序不仅从JSON中提取和映射值,还初始化复杂数据类型并执行特定于域的输入验证 . 基于反射的方法必须付出很大的努力才能完成所有这些任务 . 在评估您自己的应用程序的可用策略时,请记住这一点 . 少量重复的成本可能远远小于选择不正确的抽象 .

    import Foundation
    import UIKit
    
    struct RoomJSON
    {
        let name: String
        let room: String
    }
    
    struct Room
    {
        let name: String
        let devices: [Device]
    }
    
    struct Device
    {
        let name: String
    }
    
    extension RoomJSON: Decodable {
        enum RoomJSONKeys: String, CodingKey
        {
            case name = "name"
            case room = "room"
        }
    
        init(from decoder: Decoder) throws
        {
            let container = try decoder.container(keyedBy: RoomJSONKeys.self)
            let name: String = try container.decode(String.self, forKey: .name)
            let room: String = try container.decode(String.self, forKey: .room)
    
            self.init(name: name, room: room)
        }
    }
    
    let json = """
    [{
        "name": "TV",
        "room": "Living Room"
     },
     {
        "name": "LightBulb 1",
        "room": "Living Room"
     }]
    """.data(using: .utf8)!
    
    var rooms: [RoomJSON]?
    do {
        rooms = try JSONDecoder().decode([RoomJSON].self, from: json)
    } catch {
        print("\(error)")
    }
    
    if let rooms = rooms {
        for room in rooms {
            print(room)
        }
    }
    

相关问题