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



我的应用使用返回如下所示的 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(。
    • 我需要保留原始结构。(即从顶级user信息和子对象的信息重新创建data字典(

问题:

重新编码数据时,我无法将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>
*/

我刚刚在查看编码器协议时顿悟了。

KeyedEncodingContainerProtocol.superEncoder(forKey:)方法正是针对这种情况的。

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

对于这种特定情况,可以通过简单地调用其自己的encode(to:)方法对顶级user数据进行编码,并使用新的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>

很好的问题和解决方案,但如果你想简化它,你可以使用我写的KeyedCodable。Codable的整个实现将如下所示(当然,OrderData和User保持不变(:

struct ApiResponse<DataType: Codable>: Codable {
// MARK: Properties
var result: String!
var user: User?
var data: DataType?
enum CodingKeys: String, KeyedKey {
case result
case user = "data.user"
case data
}

}

相关内容

  • 没有找到相关文章

最新更新