Swift Codable. Swiftly.

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 the Data 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 using let data = Data(jsonString.utf8)
  • create a JSONDecoder to decode the Data, 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:

  1. If your scenario was this simple you probably wouldn’t be reading this article!
  2. 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.
  3. 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)
  4. 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 the CodingKeys 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.

Leave a Reply