json - swift 可编码 : Decode different array of items with same root objects

标签 json swift swift4 codable decodable

我目前正在尝试解码如下所示的 JSON:

{
  "result": {
    "success": true,
    "items": [
      {
        "timeEntryID": "1",
        "start": "1519558200",
        "end": "1519563600",
        "customerName": "Test-Customer",
        "projectName": "Test-Project",
        "description": "Entry 1",
      },
      {
        "timeEntryID": "2",
        "start": "1519558200",
        "end": "1519563600",
        "customerName": "Test-Customer",
        "projectName": "Test-Project",
        "description": "Entry 2",
      }
    ],
    "total": "2"
  },
  "id": "1"
}

这种特定类型的 JSON 的解码过程非常简单。我只需要这样的东西:

struct ResponseKeys: Decodable {
    let result: ResultKeys

    struct ResultKeys: Decodable {
        let success: Bool
        let items: [Item]
    }
}

现在我面临的问题是服务器的每个响应都具有与上述 JSON 相同的结构,但具有不同的项目类型。所以有时它是 let items: [Item] 但它也可能是 let items: [User] 如果我调用用户端点。

因为如果我只修改 items 数组就为每个端点编写上面的 swift 代码,那将是不必要的代码重复,所以我创建了一个自定义解码器:

enum KimaiAPIResponseKeys: String, CodingKey {
    case result

    enum KimaiResultKeys: String, CodingKey {
        case success
        case items
    }
}

struct Activity: Codable {
    let id: Int
    let description: String?
    let customerName: String
    let projectName: String
    let startDateTime: Date
    let endDateTime: Date

    enum CodingKeys: String, CodingKey {
        case id = "timeEntryID"
        case description
        case customerName
        case projectName
        case startDateTime = "start"
        case endDateTime = "end"
    }
}

extension Activity {

    init(from decoder: Decoder) throws {
        let resultContainer = try decoder.container(keyedBy: KimaiAPIResponseKeys.self)
        let itemsContainer = try resultContainer.nestedContainer(keyedBy: KimaiAPIResponseKeys.KimaiResultKeys.self, forKey: .result)
        let activityContainer = try itemsContainer.nestedContainer(keyedBy: Activity.CodingKeys.self, forKey: .items)

        id = Int(try activityContainer.decode(String.self, forKey: .id))!
        description = try activityContainer.decodeIfPresent(String.self, forKey: .description)
        customerName = try activityContainer.decode(String.self, forKey: .customerName)
        projectName = try activityContainer.decode(String.self, forKey: .projectName)
        startDateTime = Date(timeIntervalSince1970: Double(try activityContainer.decode(String.self, forKey: .startDateTime))!)
        endDateTime = Date(timeIntervalSince1970: Double(try activityContainer.decode(String.self, forKey: .endDateTime))!)
    }

}

如果 "items" 只包含一个对象而不是数组,解码器将完美运行:

{
  "result": {
    "success": true,
    "items":
      {
        "timeEntryID": "2",
        "start": "1519558200",
        "end": "1519563600",
        "customerName": "Test-Customer",
        "projectName": "Test-Project",
        "description": "Entry 2",
      },
    "total": "2"
  },
  "id": "1"
}

如果 items 是一个数组,我会得到以下错误:

typeMismatch(Swift.Dictionary, Swift.DecodingError.Context(codingPath: [__lldb_expr_151.KimaiAPIResponseKeys.result], debugDescription: "Expected to decode Dictionary but found an array instead.", underlyingError: nil))

我只是不知道如何修改我的解码器以处理一组项目。我使用 JSON 的工作和非工作版本创建了一个 Playground 文件。请看一下并尝试一下:Decodable.playground

感谢您的帮助!

最佳答案

我的建议是分别为 items 解码字典

struct Item : Decodable {

    enum CodingKeys: String, CodingKey {
        case id = "timeEntryID"
        case description, customerName, projectName
        case startDateTime = "start"
        case endDateTime = "end"
    }

    let id: Int
    let startDateTime: Date
    let endDateTime: Date
    let customerName: String
    let projectName: String
    let description: String?

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = Int(try container.decode(String.self, forKey: .id))!
        description = try container.decodeIfPresent(String.self, forKey: .description)
        customerName = try container.decode(String.self, forKey: .customerName)
        projectName = try container.decode(String.self, forKey: .projectName)
        startDateTime = Date(timeIntervalSince1970: Double(try container.decode(String.self, forKey: .startDateTime))!)
        endDateTime = Date(timeIntervalSince1970: Double(try container.decode(String.self, forKey: .endDateTime))!)
    }
}

并且在 Activity 中使用条件初始化程序,它提供了自己的 do catch block 。首先,它尝试解码单个项目并将单个项目作为数组分配给属性。如果失败,它会解码一个数组。

enum KimaiAPIResponseKeys: String, CodingKey {
    case result, id

    enum KimaiResultKeys: String, CodingKey {
        case success
        case items
    }
}

struct Activity: Decodable {
    let id: String
    let items: [Item]
}

extension Activity {

    init(from decoder: Decoder) throws {
        let rootContainer = try decoder.container(keyedBy: KimaiAPIResponseKeys.self)
        id = try rootContainer.decode(String.self, forKey: .id)
        let resultContainer = try rootContainer.nestedContainer(keyedBy: KimaiAPIResponseKeys.KimaiResultKeys.self, forKey: .result)
        do {
            let item = try resultContainer.decode(Item.self, forKey: .items)
            items = [item]
        } catch {
            items = try resultContainer.decode([Item].self, forKey: .items)
        }
    }
} 

关于json - swift 可编码 : Decode different array of items with same root objects,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48973418/

相关文章:

android - Flutter - json_serializable fromJson : The method '[]' was called on null

xcode - 侧边栏覆盖协议(protocol)问题

ios - 在两个字符之间使用正则表达式选择值

ios - Xcode 9.3 设置覆盖数据设置

javascript - 如何在Javascript中对Json数据进行排序?

javascript - Node.js 中的 json 模板引擎

ios - HTML 解析为 TextView(Swift 4 问题)

ios - 有没有办法创建一个通用的电子邮件方法(如 :- Contact us in app)

javascript - 使用 JSON xmlHttprequest 从 Web 服务器读取数据库并显示数据?

ios - Swift:在类数组中查找值的索引并过滤数组