Swift Codable:如何将顶级数据编码为嵌套容器

时间:2018-05-22 07:08:47

标签: ios swift codable decodable encodable

我的应用程序使用的服务器返回如下所示的JSON:

{
    "result":"OK",
    "data":{

        // Common to all URLs
        "user": {
            "name":"John Smith" // ETC...
        },

        // Different for each URL
        "data_for_this_url":0
    }
}

如您所见,特定于URL的信息与普通user字典存在于同一个字典中。

目标:

  1. 将此JSON解码为类/结构。
    • 因为user很常见,所以我希望它在顶级类/结构中。
  2. 编码为新格式(例如plist)。
    • 我需要保留原始结构。 (即从顶级data信息和子对象的信息中重新创建user字典)
  3. 问题:

    重新编码数据时,我无法将user字典(来自顶级对象)和特定于URL的数据(来自子对象)写入编码器。

    user覆盖其他数据,或其他数据覆盖user。我不知道如何将它们结合起来。

    这是我到目前为止所拥有的:

    // MARK: - Common User
    struct User: Codable {
        var name: String?
    }
    
    // MARK: - Abstract Response
    struct ApiResponse<DataType: Codable>: Codable {
        // MARK: Properties
        var result: String
        var user: User?
        var data: DataType?
    
        // MARK: Coding Keys
        enum CodingKeys: String, CodingKey {
            case result, data
        }
        enum DataDictKeys: String, CodingKey {
            case user
        }
    
        // MARK: Decodable
        init(from decoder: Decoder) throws {
            let baseContainer = try decoder.container(keyedBy: CodingKeys.self)
            self.result = try baseContainer.decode(String.self, forKey: .result)
            self.data = try baseContainer.decodeIfPresent(DataType.self, forKey: .data)
    
            let dataContainer = try baseContainer.nestedContainer(keyedBy: DataDictKeys.self, forKey: .data)
            self.user = try dataContainer.decodeIfPresent(User.self, forKey: .user)
        }
    
        // MARK: Encodable
        func encode(to encoder: Encoder) throws {
            var baseContainer = encoder.container(keyedBy: CodingKeys.self)
            try baseContainer.encode(self.result, forKey: .result)
    
            // MARK: - PROBLEM!!
    
            // This is overwritten
            try baseContainer.encodeIfPresent(self.data, forKey: .data)
    
            // This overwrites the previous statement
            var dataContainer = baseContainer.nestedContainer(keyedBy: DataDictKeys.self, forKey: .data)
            try dataContainer.encodeIfPresent(self.user, forKey: .user)
        }
    }
    

    示例:

    在下面的示例中,重新编码的plist不包含order_count,因为它被包含user的字典覆盖。

    // MARK: - Concrete Response
    typealias OrderDataResponse = ApiResponse<OrderData>
    
    struct OrderData: Codable {
        var orderCount: Int = 0
        enum CodingKeys: String, CodingKey {
            case orderCount = "order_count"
        }
    }
    
    
    let orderDataResponseJson = """
    {
        "result":"OK",
        "data":{
            "user":{
                "name":"John"
            },
            "order_count":10
        }
    }
    """
    
    // MARK: - Decode from JSON
    let jsonData = orderDataResponseJson.data(using: .utf8)!
    let response = try JSONDecoder().decode(OrderDataResponse.self, from: jsonData)
    
    // MARK: - Encode to PropertyList
    let plistEncoder = PropertyListEncoder()
    plistEncoder.outputFormat = .xml
    
    let plistData = try plistEncoder.encode(response)
    let plistString = String(data: plistData, encoding: .utf8)!
    
    print(plistString)
    
    // 'order_count' is not included in 'data'!
    
    /*
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
        <key>data</key>
        <dict>
            <key>user</key>
            <dict>
                <key>name</key>
                <string>John</string>
            </dict>
        </dict>
        <key>result</key>
        <string>OK</string>
    </dict>
    </plist>
    */
    

2 个答案:

答案 0 :(得分:3)

在查看编码器协议时,我刚才有了一个顿悟。

KeyedEncodingContainerProtocol.superEncoder(forKey:)方法适用于这种情况。

此方法返回一个单独的Encoder,它可以收集多个项目和/或嵌套容器,然后将它们编码为一个密钥。

对于这种特定情况,只需使用新的user调用自己的encode(to:)方法,即可对顶级superEncoder数据进行编码。然后,也可以使用编码器创建嵌套容器,以便正常使用。

问题解决方案

// MARK: - Encodable
func encode(to encoder: Encoder) throws {

    var baseContainer = encoder.container(keyedBy: CodingKeys.self)
    try baseContainer.encode(self.result, forKey: .result)

    // MARK: - PROBLEM!!
//    // This is overwritten
//    try baseContainer.encodeIfPresent(self.data, forKey: .data)
//
//    // This overwrites the previous statement
//    var dataContainer = baseContainer.nestedContainer(keyedBy: DataDictKeys.self, forKey: .data)
//    try dataContainer.encodeIfPresent(self.user, forKey: .user)

    // MARK: - Solution
    // Create a new Encoder instance to combine data from separate sources.
    let dataEncoder = baseContainer.superEncoder(forKey: .data)

    // Use the Encoder directly:
    try self.data?.encode(to: dataEncoder)

    // Create containers for manually encoding, as usual:
    var userContainer = dataEncoder.container(keyedBy: DataDictKeys.self)
    try userContainer.encodeIfPresent(self.user, forKey: .user)
}

输出:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>data</key>
    <dict>
        <key>order_count</key>
        <integer>10</integer>
        <key>user</key>
        <dict>
            <key>name</key>
            <string>John</string>
        </dict>
    </dict>
    <key>result</key>
    <string>OK</string>
</dict>
</plist>

答案 1 :(得分:-2)

很棒的问题和解决方案,但如果你想简化它,你可以使用KeyedCodable我写的。您的Codable的整个实现看起来就像那样(OrderData和User当然保持不变):

struct ApiResponse<DataType: Codable>: Codable, Keyedable {
  // MARK: Properties
  var result: String!
  var user: User?
  var data: DataType?

  mutating func map(map: KeyMap) throws {
      try result <-> map["result"]
      try user <-> map["data.user"]
      try data <-> map["data"]
  }

  init(from decoder: Decoder) throws {
      try KeyedDecoder(with: decoder).decode(to: &self)
 }

}