Serialization and deserialization in Swift iOS 27.01.2019

Serialization is a process of converting your instances to another representation, like a string or a stream of bytes. The reverse process of turning the data into an instance is called decoding, or deserialization.

So, you need to encode (or serialize) an instance before you can save it to a file or send it over the web. You need to decode (or deserialize) to bring it back from a file or the web as an instance.

The Encodable protocol is used by types that can be encoded to another representation. It declares a single method:

func encode(to: Encoder) throws

...which the compiler generates for you if all the stored properties of that type conform to Encodable as well.

The Decodable protocol is used by types that can be decoded. It declares just a single initializer:

init(from decoder: Decoder) throws

Codable is a protocol that a type can conform to, to declare that it can be encoded and decoded. It’s basically an alias for the Encodable and Decodableprotocols.

typealias Codable = Encodable & Decodable

There are many types in Swift that are codable out of the box: Int, String, Date, Array and many other types from the Standard Library and the Foundation framework. If you want your type to be codable, the simplest way to do it is by conforming to Codable and making sure all its stored properties are also codable.

For example, let’s say you have a list of movies. All you need to do to be able to encode and decode this type to conform to the Codable protocol, like so:

struct Movie: Codable {
    var title: String
    var year: Int 
}

You were able to do it because both title (String) and year (Int) are codable.

All collections types, like Array and Dictionary are also codable if they contain codable types.

JSONEncoder and JSONDecoder

You’ll learn how to encode to and decode from JSON, by using Swift’s JSONEncoder and JSONDecoder classes.

JSON stands for JavaScript Object Notation, and is one of the most popular ways to serialize data. It’s easily readable by humans and easy for computers to parse and generate.

Once you have a codable type, you can use JSONEncoder to convert your type to Data that can be either written to a file or sent over the network. Assume you have this movie instance:

let movie = Movie(name: "The Shawshank Redemption", year: 1994)

You can encode it, like so:

let jsonEncoder = JSONEncoder()
let jsonData = try jsonEncoder.encode(movie)

You’ll notice that you need to use try because encode(_:) might fail and throw an error.

If you would like to create a readable version of this JSON as a string, you can use the String initializer:

let jsonString = String(data: jsonData, encoding: .utf8)!
print(jsonString)

If you want to decode the JSON data back into an instance, you need to use JSONDecoder:

if let dataJson = jsonString?.data(using: .utf8) {
    let movie1 = try JSONDecoder().decode(Movie.self, from: dataJson)
    print(movie1)
}

Renaming properties with CodingKeys

The CodingKeys enum, which conforms to CodingKey protocol, lets you rename specific properties in case the serialized format doesn’t match the requirements of the API.

Add the nested enumeration CodingKeys like this:

struct Movie: Codable {
    var title: String
    var year: Int
    enum CodingKeys: String, CodingKey {
        case year = "annum"
        case title
    } 
}

There are several things to note here:

  1. CodingKeys is a nested enumeration in your type.
  2. It has to conform to CodingKey.
  3. You also need String as the raw type, since the keys must be either strings or integers.
  4. You have to include all properties in the enumeration, even if you don’t plan to rename them.
  5. By default, this enumeration is created by the compiler, but when you need to rename a key you need to implement it yourself.

Manual encoding and decoding

Codable is actually just a typealias for the Encodable and Decodable protocols. You need to implement encode(to: Encoder) and describe how to encode each property.

Add this extension:

extension Movie: Encodable {
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(title, forKey: .title)
        try container.encode(year, forKey: .year)
        try container.encode(people?.name, forKey: .producer)
    } 
}

Add the following code to your playground to make Movie conform to Decodable, and thus also Codable:

extension Movie: Decodable {
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        title = try values.decode(String.self, forKey: .title)
        year = try values.decode(Int.self, forKey: .year)
        if let producerTmp = try values.decode(String?.self, forKey: .producer) {
            people = Person(name: producerTmp)
        }
    } 
}

Decoding JSON dates with custom formats

Dates in JSON are defined as a String or time interval and require a conversion strategy. We can set such a strategy on our JSONDecoder, just like we did for converting camel case to snake case.

Take the following JSON example of a movie:

{
    "title": "The Shawshank Redemption",
    "date": "1994-01-21T09:15:00Z"
}

The date in this example is defined with the following format: yyyy-MM-dd'T'HH:mm:ss. We need to create a custom DateFormatter with this format and apply this to our decoder by setting the dateDecodingStrategy to formatted:

struct Movie: Decodable {
    let title: String
    let date: Date
}

let decoder = JSONDecoder()
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
dateFormatter.locale = Locale(identifier: "en_US")
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
decoder.dateDecodingStrategy = .formatted(dateFormatter)

let movie: Movie = try! decoder.decode(Movie.self, from: jsonData)

print(movie.date) // Prints: 1994-01-21 09:15:00 +0000

There are a few other strategies available to set:

  • deferredToDate. Uses Apple’s own data format that tracks the number of seconds and milliseconds since January 1st of 2001. This is mainly useful to use directly with Apple’s platforms.
  • millisecondsSince1970. This format tracks the number of seconds and milliseconds since January 1st of 1970 and is a lot more common to use.
  • secondsSince1970. Tracks the numbers of seconds since January 1st of 1970.
  • iso8601. Decodes the Date as an ISO-8601-formatted string (in RFC 3339 format).