【问题标题】:Swift Codable: Decode different array of items with same root objectsSwift Codable:解码具有相同根对象的不同项目数组
【发布时间】:2018-02-25 12:29:07
【问题描述】:

我目前正在尝试解码如下所示的 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: "期望解码字典,但找到了一个数组。",底层错误:nil))

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

感谢您的帮助!

【问题讨论】:

    标签: json swift swift4 codable decodable


    【解决方案1】:

    我的建议是分别为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)
            }
        }
    } 
    

    【讨论】:

    • 感谢您的快速回答!我认为你是对的,我需要为这些项目单独编写一个自定义解码器。如果我会实现您的答案,那么我可以解码具有单个 Item 或 Items 数组的 JSON,但我总是有一个可以包含不同类型的数组。例如用户/活动。 “项目”只是这些类型的通用术语。我怎么能完成这种行为,因为我需要在“let items: [here]”中指定类型
    • 只需将Activity 设为通用:struct Activity<Item: Decodable>,它的工作原理也一样。
    • @RobNapier 那是决定性的一点!很简单。谢谢你和瓦迪安。非常感谢您的有用回答!
    【解决方案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
    

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

    【讨论】:

    • 这正是我所做的。泛型摇滚!
    猜你喜欢
    • 2020-04-28
    • 1970-01-01
    • 2021-03-25
    • 1970-01-01
    • 2015-09-04
    • 2012-09-22
    • 1970-01-01
    • 1970-01-01
    • 2020-04-09
    相关资源
    最近更新 更多