Variable Key Names for Codable Objects: How to Make Swift Codable Protocol Even More Useful?
It’s hard to imagine modern Swift iOS application that doesn’t work with multiple data sources like servers, local cache DB, etc, or doesn’t parse/convert data between different formats. While Swift Codable protocol is a great solution for this purpose it also has some important drawbacks when developing a complex app that deals with multiple data formats. From this article, you will know how to improve the Swift Codable mechanism and why it’s important.
Swift has a great feature for encoding/decoding data in key-value formats called Coding protocol. That is, you may choose to store data in e.g. JSON format or plist by at minimum just defining names of the keys for which the corresponding values should be stored.
Advantages and Disadvantages of Swift Codable protocol
Here are the advantages of Codable protocol:
1) Type safety. You don’t need typecasting or parsing the strings read from the file. Swift does for you all the low-level reading and parsing only returning you a ready to use object of a concrete type.
2) The Simplicity of usage. At a minimum, you may just declare that your type that needs to be encodable or decodable confirms to the corresponding protocol (either Codable or it’s parts Decodable or Encodable). The compiler will match the keys from your data (e.g., JSON) automatically based on the names of your type’s properties. In case you need advanced matching of keys’ names with your type’s properties (and in most real life cases you need it), you may define an enum CodingKeys that will do the mapping.
3) Extensibility. When you need some advanced parsing, you may implement initialization and encoding methods to parse/encode the data. This, for example, allows you to decode several fields of JSON combining them into a single value or make some advanced transformation before assigning value to your codable object’s property.
Despite its flexibility, the Codable approach has a serious limitation. For real-life tasks, it’s often needed to store the same data in several data formats at the same time. For example, data coming from a server may be stored locally as a cache. Info about user account coming from the server is often stored locally to keep user sign in. At first glance, the Swift Codable protocol can be perfectly used in this case. However, the problem is that, as soon as one data source changes names of the keys for the stored values, the data won’t be readable anymore by Codable object.
As an example let’s imagine a situation when an application gets user info for a user account from the server and stores it locally to be used when the app is relaunched. In this case, the proper solution for parsing JSON data from the server into a model object is to use Codable protocol. The simplest way to store the object locally would be to just use Codable to encode the object (e.g. in plist format) and to store it locally. But codable object will use a certain set of keys that is defined by server JSON field names in our example. So if the server changes names of the JSON fields it returns, we’ll have to change Codable implementation to match the new fields’ names. So Codable implementation will use new keys to encode/decode data. And since the same implementation is used for local data, as well the user info that was previously saved locally will become unreadable.
To generalize, if we have multiple data sources for the same keyed data, the Codable implementation will stop working as soon as one of the data sources changes the names of the keys.
Approach with Multiple Entities
Let’s see how to improve the Swift Codable protocol to properly handle such a situation. We need a way to encode/decode from each data source without restriction to have the same key names. To do it, we may write a model object type for each data source.
Back to our example with server and local data, we’ll have the following code:
// Server user info
struct ServerUserInfo: Codable {
let user_name: String
let email_address: String
let user_age: Int
}
// Local user info to store in User Defaults
struct LocalUserInfo: Codable {
let USER_NAME: String
let EMAIL: String
let AGE: Int
}
So we have two different structures: one to encode/decode user info from server and the other to encode/decode data for local usage in User Defaults. But semantically, this is the same entity. So code that works with such object should be able to use any of the structures above interchangeably. For this purpose, we may declare the following protocol:
protocol UserInfo {
var userName: String { get }
var email: String { get }
var age: Int { get }
}
Each user info structure will then conform to the protocol:
extension LocalUserInfo: UserInfo {
var userName: String {
return USER_NAME
}
var email: String {
return EMAIL
}
var age: Int {
return AGE
}
}
extension ServerUserInfo: UserInfo {
var userName: String {
return user_name
}
var email: String {
return email_address
}
var age: Int {
return user_age
}
}
So, code that requires user info will use it via UserInfo
protocol.
Such solution is a very straightforward and easy to read. However, it requires much code. That is, we have to define a separate structure for each format a particular entity can be encoded/decoded from. Additionally, we need to define a protocol describing the entity and make all the structures conform to that protocol.
Approach with Variational Keys
Let’s find another approach that will make it possible to use a single structure to do the encoding/decoding from different key sets for different formats. Let’s also make this approach maintain simplicity in its usage. Obviously, we cannot have Coding keys bound to properties’ names as in the previous approach. This means we’ll need to override init(from:)
and encode(to:)
methods from Codable protocol. Below is a UserInfo
structure defined for coding in JSON format from our example.
extension UserInfo: Codable {
private enum Keys: String, CodingKey {
case userName = "user_name"
case email = "email_address"
case age = "user_age"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: Keys.self)
self.userName = try container.decode(String.self, forKey: .userName)
self.email = try container.decode(String.self, forKey: .email)
self.age = try container.decode(Int.self, forKey: .age)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: Keys.self)
try container.encode(userName, forKey: .userName)
try container.encode(email, forKey: .email)
try container.encode(age, forKey: .age)
}
}
In fact, to make the code above decode and encode another data format we only need to change the keys themselves. That is, we’ve used simple enum conforming to the CodingKey
protocol to define the keys. However, we may implement arbitrary type conforming to the CodingKey
protocol. For example, we may choose a structure. So, a particular instance of a structure will represent the coding key used in calls to container.decode()
or container.encode()
. While implementation will provide info about the keys of a particular data format. The code of such structure is provided below:
struct StringKey: CodingKey {
let stringValue: String
let intValue: Int?
init?(stringValue: String) {
self.intValue = nil
self.stringValue = stringValue
}
init?(intValue: Int) {
self.intValue = intValue
self.stringValue = "\(intValue)"
}
}
So, the StringKey
just wraps a concrete key for a particular data format. For example, to decode userName from JSON, we’ll create the corresponding StringKey
instances specifying JSON user_name
field into init?(stringValue:)
method.
Now we need to find a way to define key sets for each data type. To each property from UserInfo
, we need somehow assign keys that can be used to encode/decode the property’s value. E.g. for property userName
corresponds to user_name
key for JSON and USER_NAME
key for plist format. To represent each property, we may use Swift’s KeyPath
type. Also, we would like to store information about which data format each key is used for. Translating the above into code we’ll have the following:
enum CodingType {
case local
case remote
}
extension UserInfo {
static let keySet: [CodingType: [PartialKeyPath<UserInfo>: String]] = [
// for .plist stored locally
.local: [
\Self.userName: "USER_NAME",
\Self.email: "EMAIL",
\Self.age: "AGE"
],
// for JSON received from server
.remote: [
\Self.userName: "user_name",
\Self.email: "email_address",
\Self.age: "user_age"
]
]
}
To let the code inside init(from:)
and encode(to:)
methods aware of the decode/encode data format we may use user info from Decoder/Encoder
objects:
extension CodingUserInfoKey {
static var codingTypeKey = CodingUserInfoKey(rawValue: "CodingType")
}
...
let providedType = <either .local or .remote from CodingType enum>
let decoder = JSONDecoder()
if let typeKey = CodingUserInfoKey.codingTypeKey {
decoder.userInfo[typeKey] = providedType
}
When decoding/encoding, we’ll just read the value from user info for CodingUserInfoKey.codingTypeKey
key and pick the corresponding set of coding keys.
Let’s bring all the above together and see how our code will look like:
enum CodingError: Error {
case keyNotFound
case keySetNotFound
}
extension UserInfo: Codable {
static func codingKey(for keyPath: PartialKeyPath<Self>,
in keySet: [PartialKeyPath<Self>: String]) throws -> StringKey {
guard let value = keySet[keyPath],
let codingKey = StringKey(stringValue: value) else {
throw CodingError.keyNotFound
}
return codingKey
}
static func keySet(from userInfo: [CodingUserInfoKey: Any]) throws -> [PartialKeyPath<Self>: String] {
guard let typeKey = CodingUserInfoKey.codingTypeKey,
let type = userInfo[typeKey] as? CodingType,
let keySet = Self.keySets[type] else {
throw CodingError.keySetNotFound
}
return keySet
}
init(from decoder: Decoder) throws {
let keySet = try Self.keySet(from: decoder.userInfo)
let container = try decoder.container(keyedBy: StringKey.self)
self.userName = try container.decode(String.self, forKey: try Self.codingKey(for: \Self.userName,
in: keySet))
self.email = try container.decode(String.self, forKey: try Self.codingKey(for: \Self.email,
in: keySet))
self.age = try container.decode(Int.self, forKey: try Self.codingKey(for: \Self.age,
in: keySet))
}
func encode(to encoder: Encoder) throws {
let keySet = try Self.keySet(from: encoder.userInfo)
var container = encoder.container(keyedBy: StringKey.self)
try container.encode(userName, forKey: try Self.codingKey(for: \Self.userName,
in: keySet))
try container.encode(email, forKey: try Self.codingKey(for: \Self.email,
in: keySet))
try container.encode(age, forKey: try Self.codingKey(for:
\Self.age,
in: keySet))
}
}
Note we’ve added two helper static methods: codingKey(for keyPath
, in keySet)
and keySet(from userInfo)
. Their usage makes code of init(from:)
and encode(to:)
more clear and straightforward.
Generalizing the Solution
Let’s improve the solution with coding key sets we’ve developed to make it easier and faster to apply. The solution has some boilerplate code for transforming KeyPath of the type into a coding key and choosing the particular key set. Also, encoding/ decoding code has a repeating call to codingKey(for keyPath, in keySet)
that complicates the init(from:)
and encode(to:)
implementation and can be reduced.
First, we’ll extract helping code into helper objects. It will be enough to just use structures for this purpose:
private protocol CodingKeyContainable {
associatedtype Coding
var keySet: [PartialKeyPath<Coding>: String] { get }
}
private extension CodingKeyContainable {
func codingKey(for keyPath: PartialKeyPath<Coding>) throws -> StringKey {
guard let value = keySet[keyPath], let codingKey = StringKey(stringValue: value) else {
throw CodingError.keyNotFound
}
return codingKey
}
}
struct DecodingContainer<CodingType>: CodingKeyContainable {
fileprivate let keySet: [PartialKeyPath<CodingType>: String]
fileprivate let container: KeyedDecodingContainer<StringKey>
func decodeValue<PropertyType: Decodable>(for keyPath: KeyPath<CodingType, PropertyType>) throws -> PropertyType {
try container.decode(PropertyType.self, forKey: try codingKey(for: keyPath as PartialKeyPath<CodingType>))
}
}
struct EncodingContainer<CodingType>: CodingKeyContainable {
fileprivate let keySet: [PartialKeyPath<CodingType>: String]
fileprivate var container: KeyedEncodingContainer<StringKey>
mutating func encodeValue<PropertyType: Encodable>(_ value: PropertyType, for keyPath: KeyPath<CodingType, PropertyType>) throws {
try container.encode(value, forKey: try codingKey(for: keyPath as PartialKeyPath<CodingType>))
}
}
Protocol CodingKeyContainable
just helps us to reuse key set retrieving code in both structures.
Now let’s define our own Decodable/Encodable-like protocols. This will allow us to hide all the boilerplate code for getting the proper key set and creating a decoder/encoder object inside of the default implementation of init(from:)
and encode(to:)
methods. On the other hand, it will allow us to simplify decoding/encoding the concrete values by using DecodingContainer
and EncodingContainer
structures we’ve defined above. Another important thing is that by using the protocols, we’ll also add the requirement of implementing:
static let keySet: [CodingType: [PartialKeyPath<UserInfo>: String]]
by codable types for which we want to use the approach with variational keys.
Here are our protocols:
// MARK: - Key Sets
protocol VariableCodingKeys {
static var keySets: [CodingType: [PartialKeyPath<Self>: String]] { get }
}
private extension VariableCodingKeys {
static func keySet(from userInfo: [CodingUserInfoKey: Any]) throws -> [PartialKeyPath<Self>: String] {
guard let typeKey = CodingUserInfoKey.codingTypeKey,
let type = userInfo[typeKey] as? CodingType,
let keySet = Self.keySets[type] else {
throw CodingError.keySetNotFound
}
return keySet
}
}
// MARK: - VariablyDecodable
protocol VariablyDecodable: VariableCodingKeys, Decodable {
init(from decodingContainer: DecodingContainer<Self>) throws
}
extension VariablyDecodable {
init(from decoder: Decoder) throws {
let keySet = try Self.keySet(from: decoder.userInfo)
let container = try decoder.container(keyedBy: StringKey.self)
let decodingContainer = DecodingContainer<Self>(keySet: keySet, container: container)
try self.init(from: decodingContainer)
}
}
// MARK: - VariablyEncodable
protocol VariablyEncodable: VariableCodingKeys, Encodable {
func encode(to encodingContainer: inout EncodingContainer<Self>) throws
}
extension VariablyEncodable {
func encode(to encoder: Encoder) throws {
let keySet = try Self.keySet(from: encoder.userInfo)
let container = encoder.container(keyedBy: StringKey.self)
var encodingContainer = EncodingContainer<Self>(keySet: keySet, container: container)
try self.encode(to: &encodingContainer)
}
}
typealias VariablyCodable = VariablyDecodable & VariablyEncodable
Let’s now rewrite our UserInfo
structure to make it conform to newly defined VariablyCodable
protocol:
extension UserInfo: VariablyCodable {
static let keySets: [CodingType: [PartialKeyPath<UserInfo>: String]] = [
// for .plist stored locally
.local: [
\Self.userName: "USER_NAME",
\Self.email: "EMAIL",
\Self.age: "AGE"
],
// for JSON received from server
.remote: [
\Self.userName: "user_name",
\Self.email: "email_address",
\Self.age: "user_age"
]
]
init(from decodingContainer: DecodingContainer<UserInfo>) throws {
self.userName = try decodingContainer.decodeValue(for: \.userName)
self.email = try decodingContainer.decodeValue(for: \.email)
self.age = try decodingContainer.decodeValue(for: \.age)
}
func encode(to encodingContainer: inout EncodingContainer<UserInfo>) throws {
try encodingContainer.encodeValue(userName, for: \.userName)
try encodingContainer.encodeValue(email, for: \.email)
try encodingContainer.encodeValue(age, for: \.age)
}
}
This is where a true power of protocols comes. By conforming to VariablyCodable
our type automatically becomes Codable. Moreover, without any boilerplate code, we now have the ability to use different sets of coding keys.
Going back to the advantages of the Codable protocol we outlined at the beginning of the article, let’s check which ones VariablyCodable
has.
1) Type safety. Nothing changed here comparing to the Codable protocol. VariablyCodable
protocol still uses concrete types without involving any dynamic type casting.
2) The simplicity of usage. Here we don’t have declarative style option with enum describing keys and values. We always have to implement init(from:)
and encode(to:)
methods. However, since the minimum implementation of the methods is so simple and straightforward (each line just decodes/encodes single property) that it is comparable to defining CodingKeys
enum for the Codable protocol.
3) Extensibility. Here we have more abilities comparing to the Codable protocol. Additionally to the flexibility that can be achieved by implementing init(from:)
and encode(to:)
methods, we have also keySets
map that provides an additional layer of abstraction of coding keys.
Summary
We defined two approaches to extend the behavior of the Codable protocol in Swift to be able to use a different set of keys for different data formats. The first approach implying separate types for each data format works well for simple cases when having two data formats and a single data flow direction (e.g. decoding only). However, if your app has multiple data sources and encodes/decodes arbitrarily between those formats you may stick to approach with VariablyCodable
protocol. While it needs more code to be written at the beginning, once implemented, you will gain great flexibility and extensibility in coding/decoding data for any type you need.
Check related articles
Read our blog and stay informed about the industry's latest trends and solutions.
see all articles