Swift Codable:如何将***数据编码到嵌套容器中
Posted
技术标签:
【中文标题】Swift Codable:如何将***数据编码到嵌套容器中【英文标题】:Swift Codable: How to encode top-level data into nested container 【发布时间】:2018-10-31 20:55:02 【问题描述】:我的应用使用返回 JSON 的服务器,如下所示:
"result":"OK",
"data":
// Common to all URLs
"user":
"name":"John Smith" // ETC...
,
// Different for each URL
"data_for_this_url":0
如您所见,特定于 URL 的信息与常见的 user
字典存在于同一字典中。
目标:
-
将此 JSON 解码为类/结构。
因为
user
很常见,所以我希望它位于***类/结构中。
编码为新格式(例如 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>
*/
【问题讨论】:
你不应该首先改变数据的结构。您的所有回复都包含相似的结果应该不是问题。根据 JSON 结构定义结构。您将能够非常轻松地对它们进行编码-解码。 我什至同意你的看法。无论如何,我可能最终会这样做。但是现在,我只是想知道这是否可以做到。 另外,练习这样的奇怪案例可以帮助我加深对技术的理解,这一直是我的目标。 【参考方案1】:很好的问题和解决方案,但如果您想简化它,您可以使用我写的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
【讨论】:
你已经展示了你的库的解码能力。但问题具体是关于扭转这个过程。您的图书馆能否将数据重新编码为其原始结构?如果有,你能举个例子吗? 就是这样 :) 它是 Codable 实现,它适用于解码和编码。对于正常工作的证明,您可以查看 UnitTests,尤其是提交 InnerTest。你的例子是第二个测试。它的工作原理是这样的:1)解码 json,检查属性 2)将 obj(1)编码回字符串 3)解码 json(2)检查属性 4)将 obj(3)编码为第二个字符串 5)检查来自 2 和 4 的字符串equal 当然整个测试以成功结束。让我知道您对此有何看法。 我明白了。您已经通过协议扩展实现了Encodable
,而init
方法无法做到这一点。问题主要与标准Codable
方式有关,但我会记住这一点。绝对看起来像一个可用的库。干得好。
我也认为 Apples .nestedContainer
方法使用起来太复杂了,谢谢你的图书馆——特别是我正在寻找的东西!我不明白为什么有人对你投了反对票,我给了你我的大拇指!继续努力!【参考方案2】:
我刚刚在浏览编码器协议时顿悟了。
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>
【讨论】:
以上是关于Swift Codable:如何将***数据编码到嵌套容器中的主要内容,如果未能解决你的问题,请参考以下文章
如何使用 Swift 创建 JSON Codable [关闭]
Swift之Codable自定义解析将任意数据类型解析为想要的类型
Swift之Codable自定义解析将任意数据类型解析为想要的类型