Like most developers I use Codable
(Encodable
and Decodable
) all the time for handling data supplied through web APIs and to persist small amounts of data locally as files. A lot of the time I can rely on the compiler to synthesise the Codable
implementation for me, but those times when I can’t I always forget the nuances of doing it manually and have to check. This usually involves finding a long reference or tutorial article and digging through it for the small bit of help I need.
This is my quick reference guide for those times; it may help some others out there. This article won’t explore all possible alternatives for coding strategies, edge cases, etc., and definitely isn’t designed for the novice, but should provide enough for 80% of the times an experienced developer need to roll their own, and let them do the other 20% with quick references to XCode’s documentation.
This article focuses on Decodable
, but should also provide enough clues for Encodable
(I may add this explicitly at a future point).
The Overall Process
The process for decoding is three steps:
- Get the data, and convert it into a
Data
object. The data could be from a local file or REST API. This article largely takes this bit as a given. - Map the JSON external data to the internal data representation.
- Use the
JSONDecoder
to decode theData
into the internal data objects.
Mapping the Data
For starters, create an internal data structure that can represent the data from your JSON, which in all likelihood will be a struct. If the JSON data comprises of common data types that already conform to Decodable and the internal property names match the JSON keys, then the struct automatically conforms to Decodable
and there is little to do as the compiler will synthesise most things required. If there’s a chance that some of the properties won’t be present in the JSON make them optionals.
Small differences like JSON’s common use of Snake_Case and Swift’s use of camelCase can be handled by the Decoder by just setting the appropriate options. For example, to accommodate snake_case:
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
In the real world it’s common for the JSON to provide data that is not required within our app. To selectively extract data from the JSON add a CodingKeys
enum to the data struct to list the JSON fields required. The CodingKeys
enum needs to conform to the CodingKey
protocol, but this requires nothing more than stating that compliance.
struct Customer: Decodable
enum CodingKeys: CodingKey {
case field1, field2, field5
}
var field1: String
var field2: Int
var field5: String
}
If there’s a possibility that a data field might be missing in the JSON, make it optional in your Swift data type otherwise the decoder will throw an error if it’s missing when it comes to decode.
It’s rare that internal and external data naming schemes match, and it’ll probably be necessary to map between internal property names and the JSON keys. To do this make CodingKeys String-compliant and assign each enum case a value that matches the JSON key.
struct Customer: Decodable {
enum CodingKeys: String, CodingKey {
case name = "customer_name_preferred"
case age = "customer_age_at_current_date"
case salutation = "customer_name_prefix"
}
var name: String
var age: Int
var salutation: String
}
This all works fine for simple JSON but, again back in the real world, the JSON probably contain nested levels of data.
One option would be to create corresponding nested data types in the internal data structure. For example, our JSON could reasonably contain an address for the Customer, provided as a nested structure containing postal (zip) code and house number amongst other address information. This could be modelled in our data structure with a nested Address struct
(with its own CodingKeys
entry).
struct Customer: Decodable {
struct Address: Decodable {
enum CodingKeys: String, CodingKey {
case zip = "customer_zipcode"
case number = "customer_house_number
}
case zip: String
case number: String
}
enum CodingKeys: String, CodingKey {
case name = "customer_name_preferred"
case age = "customer_age_at_current_date"
case salutation = "customer_name_prefix"
}
var name: String
var age: Int
var salutation: String
var address: Address
}
This approach allows the decoder to automatically decode the JSON directly into the data structure. While in this simple example this approach might make sense, it is rarely practical to do this with real data as it will likely result in unwieldy nested data structures. In practice it will be often more appropriate to flatten the JSON structure when decoding it. Complicating an internal data model just to mirror the JSON structure is rarely a good design decision.
The decoder can help with flattening the JSON hierarchy but it does mean writing a custom .init(from:) method for the decoder rather than allowing the compiler to synthesise it. It will also mean providing the corresponding mapping in the CodingKeys enum.
In this contrived example, only the house number and postcode are required from potentially a larger array of address fields, so it make sense to flatten the JSON address structure so these are simple properties in the Customer struct. To achieve this, add an enum case for the the nested JSON block to CodingKeys and then create a second enum to represent the nested fields to decode. To demonstrate handling missing JSON data later in this post, the house number has been made optional
struct Customer: Decodable {
enum CodingKeys: String, CodingKey {
case name = "customer_name_preferred"
case age = "customer_age_at_current_date"
case salutation = "customer_name_prefix"
case address = "customer_address_fields"
enum AddressFields: String, CodingKey {
case number = "customer_house_number"
case zip = "customer_zipcode"
}
}
var name: String
var age: Int
var salutation: String
var houseNumber: String?
var postCode: String
}
The CodingKeys
enum still represents the underlying JSON fields we want to retrieve, and their hierarchy, but is not mirrored directly in our data structure. As the compiler is no longer synthesising the Codable compliance there is no requirement to stick to the CodingKeys
name for the enum describing our JSON data, and we can use more descriptive names which, in complex, multi-level JSON can make the code we write to handle the decoding far easier to read. For the sake of convention I tend to keep the top-level enum called CodingKeys
as not doing so would probably lead to head-scratching in the future!
Having created our data structure and determined how we are going to map this to the JSON, we can now go on and look at decoding.
The Customer Decoder: Converting JSON into the Data Structure
There are three simple steps to decoding JSON
- convert the JSON string to a
Data
object usinglet data = Data(jsonString.utf8)
- create a
JSONDecoder
to decode theData
, setting decoding parameters as required. If the app is decoding a JSON file it created then the defaults will be fine; if it’s consuming a REST API, which is likely on a UNIX-based web server, these will probably need specifying. A common example of such an option is to support snake_case. Check the Apple documentation for all the details and options. - use the decoder to decode the
Data
into your data type.
In a simple implementation, as for the early examples above, the compiler can synthesise all the necessary JSONDecoder
methods and the whole thing this looks like.
let jsonData = jsonString.data(using: .utf8)
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .secondsSince1970
let myData = decoder.decode(MyDataType.self, from: jsonData)
A few quick asides:
- If your scenario was this simple you probably wouldn’t be reading this article!
- Anyone who is being a good citizen will, at this point, create the corresponding unit test to validate a known JSON input string gives the correct output.
- Dates! How does something as “simple” as a date always cause such problems when coding. Apple provides a number of the standard data formats to aid decoding JSON date fields, but you’ll often find that implementations of standards vary and that the Apple-supplied date formats fail. In this case there’s a fallback: pass in a DateFormatter object with the format string, calendar, time zone, locale, etc defined in it and use it like this:
decoder.dateDecodingStrategy = .formatted(customDateFormatter)
- If you’re going to need it regularly, consider adding it as a static property to the type, or, better still, to the DateFormatter itself through an extension.
When doing more than just directly converting JSON into an equivalent data structure, possibly with renaming or omitting some keys via CodingKeys
, a custom JSONDecoder.init(from: decoder: Decoder)
is required. Use this to:
- create a keyed decoding container, based on a
CodingKeys
enum, specifically for the JSON structure - use that container to extract values from the data object
- handle nested structures in the JSON by creating a
nested container
within the decoder, based on theCodingKeys
for the nested JSON, and use that to extract the values - save the extracted data back to the struct’s properties
extension Customer {
init(from decoder: Decoder) throws {
let container = try deocder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
age = try containter.decode(Int.self, forKey: .age)
salutation = try. container.decode(String.self, forKey: .salutation)
//now deal with the nested JSON
let addressContainer = try container.nestedContainer(keyedBy: AddressFields.self, forKey: .address)
houseNumber = try addressContainer.decodeIfPresent(String.self, forKey: .number)
postCode = try addressContainer.decode(String.self, forKey: .zip)
}
}
This custom decoder takes the hierarchical JSON, mapped by the two CodingKeys
enums, and then flattens it by extracting the data and writing them back to the simple struct properties. For further nested data could be processed by creating further CodingKeys
-type enums as required and using them to create further nestedContainer
objects to extract the data.
Decoding arrays
Decoding arrays is only slightly more complex, but it does depend on the array’s elements also conforming to Codable
. If they don’t, start by implementing this compliance. In the simplest scenario, arrays can be decoded directly, treating them as an array of the underlying Codable
type.concrete types:
let decoder = JSONDcoder()
let array = decoder.decode([DecodableType].self, from: jsonData)
This works fine for simple arrays but can get messy and may involve creating interim data structures to decode Data containing arrays of more complex data where there are nested arrays.
An alternative approach for arrays is to use an unkeyed container
to extract each array member in sequence and handle it directly.
For example, if the JSON had an array of strings nested under the key customer_discount_codes
that provide valid discount codes the customer could use and this was modelled as var validCodes: [String]
in our Customer struct, an unkeyed container
could be used to decode it.
Modify the Customer struct’s ‘CodingKeys’ to:
enum CodingKeys: String, CodingKey {
case name = "customer_name_preferred"
case age = "customer_age_at_current_date"
case salutation = "customer_name_prefix"
case address = "customer_address_fields"
case discountCodes = "customer_discount_codes"
enum AddressFields: String, CodingKey {
case number = "customer_house_number"
case zip = "customer_zip"
}
enum CodeFields: String, CodingKey {
case code = "customer_discount_code"
}
}
and then extend the init(from decoder: Decoder)
method to:
init(from decoder: Decoder) throws {
let container = try deocder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
age = try containter.decode(Int.self, forKey: .age)
salutation = try. container.decode(String.self, forKey: .salutation)
//now deal with the nested JSON
let addressContainer = try container.nestedContainer(keyedBy: AddressFields.self, forKey: .address)
houseNumber = try addressContainer.decodeIfPresent(String.self, forKey: .number)
postCode = try addressContainer.decode(String.self, forKey: .zip)
//process the nested discounts array with an unkeyed container
var codesContainer = try container.nestedUnkeyedContainer(forKey: .discountCodes)
var validCodes: [String]()
while !codesContainer.isAtEnd {
codesContainer = try codesContainer.nestedContainer{keyedBy: CodeFields.self)
let code = try codesContainer.decode(String.self, forKey: .code)
validCodes.append(code)
}
}
This approach works well, but using a mutating method that returns a version of the container but with the start index pointing the next element always feels ‘dirty’, not very swift-like, and not immediately obvious as to what’s going on whenever I come back to it. Whenever practical I prefer to decode as an array of concrete types, even if it means creating a temporary type just for the purpose. The code and possible performance overhead usually seems worth it for future readability/maintainability.