My app uses a server that returns JSON that looks like this:
{
"result":"OK",
"data":{
// Common to all URLs
"user": {
"name":"John Smith" // ETC...
},
// Different for each URL
"data_for_this_url":0
}
}
As you can see, the URL-specific info exists in the same dictionary as the common user
dictionary.
GOAL:
- Decode this JSON into classes/structs.
- Because
user
is common, I want this to be in the top-level class/struct.
- Because
- Encode to new format (e.g. plist).
- I need to preserve the original structure. (i.e. recreate the
data
dictionary from top-leveluser
info and child object's info)
- I need to preserve the original structure. (i.e. recreate the
PROBLEM:
When re-encoding the data, I cannot write both the user
dictionary (from top-level object) and URL-specific data (from child object) to the encoder.
Either user
overwrites the other data, or the other data overwrites user
. I don't know how to combine them.
Here's what I have so far:
// 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)
}
}
EXAMPLE:
In the example below, the re-encoded plist does not include order_count
, because it was overwritten by the dictionary containing 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>
*/