Reputation: 1678
I have types for which I provide CodingKeys
with custom names for their data members as appropriate. I would like to encode, as required by different call sites, only a subset of a type's CodingKeys
before sending the data to a server. The required encodings change from call site to call site, so there's no default implementation to consider.
Example types (for demonstration purposes):
struct Account: Codable
{
let name: String;
var balance: Float;
mutating func set(balance: Float)
{
self.balance = balance; // verifications, etc.
}
}
struct User: Codable
{
enum CodingKeys: String, CodingKey
{
case
id = "db_id",
name = "db_name",
account
// many more keys
}
let id: Int;
let name: String;
var account: Account;
// many more data members
}
Creating an instance:
var user = User(
id: 1, name: "john", account: Account(name: "checking", balance: 10_000));
Using a JSONEncoder
works as expected; it produces the following:
{
"db_id" : 1,
"db_name" : "john",
"account" : {
"balance" : 10000,
"name" : "checking"
}
}
I want to encode a subset of the User
type in order to send that data back to a server so that I can update specific data fields instead of updating the entire set of properties of my type. Mock usage example:
user.account.set(balance: 15_000);
let jsonEncoding = JSONEncodeSubset.of(
user, // instance to encode
keys: [User.CodingKeys.id, User.CodingKeys.account] // data to include
);
The resulting produced JSON would look like so:
{
"db_id" : 1,
"account" : {
"balance" : 15000,
"name" : "checking"
}
}
Server side, we now have the exact data we need to perform our desired update.
Another example: somebody entered the wrong name for our user, therefore another update request looks like this:
user.name = "jon"; // assume the model was modified to make this mutable
let jsonEncoding = JSONEncodeSubset.of(
user, // instance to encode
keys: [User.CodingKeys.id, User.CodingKeys.name] // only encode id & name
);
The expected resulting JSON encoding:
{
"db_id" : 1,
"name" : "jon"
}
Note that we exclude the information that isn't part of the update (user's account). The objective is to optimize the encoding to include only data that is relevant to that specific request.
Considering I have a large list of objects to update, with different call sites updating different things, I'd like to have a succinct way to perform the encoding task.
Does Swift provide any such support for encoding subsets of a type's CodingKeys
?
Upvotes: 7
Views: 806
Reputation: 54426
Expanding on Joakim Danielson's answer you can use CodingUserInfoKey
to pass data to userInfo
property of JSONEncoder
.
extension CodingUserInfoKey {
static let keysToEncode = CodingUserInfoKey(rawValue: "keysToEncode")!
}
extension JSONEncoder {
func withEncodeSubset<CodingKeys>(keysToEncode: [CodingKeys]) -> JSONEncoder {
userInfo[.keysToEncode] = keysToEncode
return self
}
}
You need to make CodingKeys
conform to CaseIterable
and implement a custom encode(to:)
method:
struct User: Codable {
enum CodingKeys: String, CodingKey, CaseIterable { // add `CaseIterable`
...
}
...
func encode(to encoder: Encoder) throws {
let keysToEncode = encoder.userInfo[.keysToEncode] as? [CodingKeys] ?? CodingKeys.allCases
var container = encoder.container(keyedBy: CodingKeys.self)
for key in keysToEncode {
switch key {
case .id:
try container.encode(id, forKey: .id)
case .account:
try container.encode(account, forKey: .account)
case .name:
try container.encode(name, forKey: .name)
}
}
}
}
And use it like this:
let encoder = JSONEncoder().withEncodeSubset(keysToEncode: [User.CodingKeys.id, .account])
let encoded = try encoder.encode(user)
print(String(data: encoded, encoding: .utf8)!)
// prints: {"db_id":1,"account":{"name":"checking","balance":15000}}
Upvotes: 4
Reputation: 51871
This solution requires that you write a custom encode(to:)
for all properties but once that is done it should be easy to use.
The idea is to let the encode(to:)
encode only those properties/keys that exists in a given array. So given the example above the function would look like this
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
for key in selectedCodingKeys {
switch key {
case .id:
try container.encode(id, forKey: .id)
case .account:
try container.encode(account, forKey: .account)
case .name:
try container.encode(name, forKey: .name)
}
}
}
selectedCodingKeys
is new property in the struct
var selectedCodingKeys = [CodingKeys]()
and we could also add a specific function for encoding
mutating func encode(for codingKeys: [CodingKeys]) throws -> Data {
self.selectedCodingKeys = codingKeys
return try JSONEncoder().encode(self)
}
and then the decoding could be done like in this example
var user = User(id: 1, name: "John", account: Account(name: "main", balance: 100.0))
do {
let data1 = try user.encode(for: [.id, .account])
let data2 = try user.encode(for: [.id, .name])
} catch {
print(error)
}
Upvotes: 3