基于 Swift 可解码对象属性中的键的模型关系

Model relations based on a key in the attributes of a Swift Decodable object

鉴于 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™ 那样做。有没有更聪明的方法?

我在这里做一个假设 - "the Swift 4 Decodable way of decoding JSON" 你的意思是调用 try JSONDecoder().decode([Room].self, from: jsonData)。 如果是这种情况,那么,据我所知,你就不走运了,因为 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: { [=10=].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: { [=11=].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 会根据出错的地方给你很好的、具体的错误! :)

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

为了配合 Eivind,因为他已经写了一个很好的答案,我将添加我的 2 美分...JSON 是一个对象模型,所以我会解码 JSON 对象,然后将这些对象转换为 first-class Swift 对象。只要服务器在说话 JSON 你就必须假设它会在某个时候发生变化,你不想要的一件事是 JSON 对象模型渗入或口述对象结构甚至变量名称Swift 世界。因此,将对象解码为 Plain Ol' Swift 对象 (POSO) 和类型,然后进行扩展以处理从这些 POSO 解码器到您将围绕其构建应用程序的对象的转换是一个完美的选择。我工作的操场在下面,但 Eivind 先发制人,在他的第二个例子中麻烦地生成了最终的纯 Swift 对象。

Apple 关于 JSON 处理的博客引用了这句好话

Working with JSON in Swift

Converting between representations of the same data in order to communicate between different systems is a tedious, albeit necessary, task for writing software.

Because the structure of these representations can be quite similar, it may be tempting to create a higher-level abstraction to automatically map between these different representations. For instance, a type might define a mapping between snake_case JSON keys and camelCase property names in order to automatically initialize a model from JSON using the Swift reflection APIs, such as Mirror.

However, we’ve found that these kinds of abstractions tend not to offer significant benefits over conventional usage of Swift language features, and instead make it more difficult to debug problems or handle edge cases. In the example above, the initializer not only extracts and maps values from JSON, but also initializes complex data types and performs domain-specific input validation. A reflection-based approach would have to go to great lengths in order to accomplish all of these tasks. Keep this in mind when evaluating the available strategies for your own app. The cost of small amounts of duplication may be significantly less than picking the incorrect abstraction.

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