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:
user
is common, I want this to be in the top-level class/struct.data
dictionary from top-level user
info and child object's info)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>
*/
It only automatically works for one type of formatted date. If the JSON provides a date with a non-default format, the codable protocol will fail.
Codable; the data-parsing dream come true!Codable is the combined protocol of Swift's Decodable and Encodable protocols. Together they provide standard methods of decoding data for custom types and encoding data to be saved or transferred.
Codable is a type alias for the Encodable and Decodable protocols. When you use Codable as a type or a generic constraint, it matches any type that conforms to both protocols.
I just now had an epiphany while looking through the encoder protocols.
KeyedEncodingContainerProtocol.superEncoder(forKey:)
method is for exactly this type of situation.
This method returns a separate Encoder
that can collect several items and/or nested containers and then encode them into a single key.
For this specific case, the top-level user
data can be encoded by simply calling its own encode(to:)
method, with the new superEncoder
. Then, nested containers can also be created with the encoder, to be used as normal.
Solution to Question
// 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)
}
Output:
<?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>
Great question and solution but if you would like to simplify it you may use KeyedCodable I wrote. Whole implementation of your Codable's will look like that (OrderData and User remain the same of course):
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
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With