3

我目前正在尝试解码如下所示的 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 相同的结构,但具有不同的项目类型。所以有时是这样,但如果我调用 User 端点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: "期望解码 Dictionary 但找到了一个数组。",底层错误: nil))

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

谢谢您的帮助!

4

2 回答 2

3

我的建议是分别解码字典/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块。首先,它尝试解码单个项目并将单个项目作为数组分配给属性。如果失败,它会解码一个数组。

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)
        }
    }
} 
于 2018-02-25T13:13:06.337 回答
2

你可以使用泛型,这是处理这种情况的一种巧妙方法。

  struct MainClass<T: Codable>: Codable  {
     let result: Result<T>
     let id: String
  }

  struct Result <T: Codable>: Codable {
     let success: Bool
     let items: [T]
     let total: String
  }

在这里你会得到物品

   let data = Data()
   let decoder = JSONDecoder()
   let modelObjet = try! decoder.decode(MainClass<User>.self, from: data)
   let users = modelObjet.result.items

在我看来,泛型是处理这种情况下重复代码的最佳方法。

于 2019-03-14T09:40:29.843 回答