Encoding and Decoding in Swift 4

Encoding and Decoding in Swift 4
Photo by Markus Spiske / Unsplash

New Capabilities Work With Native Swift Types

Structs, enums, and classes are all now able to take advantage of customizable, automatic encoding and decoding functionality in Swift 4.

Swift 4 adds new protocols, Encodable and Decodable (and for convenience, a typealias, Codable, defined as Encodable & Decodable) that define the behavior necessary for objects to encode and decode themselves (see SE-0167: Swift Encoders). This is a very welcome addition. Previously, support for these capabilities was provided only through the Foundation framework's NSCoding protocol, and was therefore limited in Swift codebases to subclasses of NSObject.

Swift 4 also adds classes that provide encoder and decoder implementations for JSON and property lists (JSONEncoder, JSONDecoder, PropertyListEncoder, and PropertyListDecoder). Codable support has been added to Foundation classes such as Date, Data, and URL, and is directly supported by Swift Library types such as String, Int, and Double, making coding easy to support for custom types. In many cases, Codable conformance requires nothing more than adding the protocol name to a custom type's protocol list.

Types that conform to the Codable protocol can encode/decode themselves automatically. For example, the following Swift struct

struct Person: Codable {
    var name: String
    var age: Int
}

requires no additional code in order to encode itself. Instead, you simply instantiate an encoder and call its encode(to:)method.

// Given an instance of Person...
let fred = Person(name: "Fred", age: 30)

// Instantiate an encoder
let encoder = JSONEncoder()

// Pass the Person instance to the encoder's encode(to:) method
let data = try! encoder.encode(fred)

And that's it! Because all of Person's properties are Codable, nothing further was needed. The result is the following JSON:

{
  "name" : "Fred",
  "age" : 30,
}

Decoding from JSON or plist data is equally simple:

//Instantiate a decoder
let decoder = JSONDecoder()

// Pass the instance type and JSON data to the decoder's decode(from:) method.
let fredsClone = try! decoder.decode(Person.self, from: data)

The previous line constructs a new Person instance by calling init(from decoder:), an initializer defined in the Decodable protocol and implemented via a protocol extension in the Swift 4 Library.

Working with Nested Objects

Since Codable properties are handled automaticaly, you can nest objects of any custom type that conforms to Codablewithout writing any additional code. For example suppose you want to modify Person to include a property of type Dog, and that one of Dog's properties is an enum named Breed, as shown below:

struct Dog: Codable {
    var name: String
    var breed: Breed

    // Note that the Breed enum adopts Codable, which is
    // automatically supported for enums that have raw values.
    enum Breed: String, Codable {
        case collie = "Collie"
        case beagle = "Beagle"
        case greatDane = "Great Dane"
    }
}

struct Person: Codable {
    var name: String
    var age: Int
    var dog: Dog
}

Because both Dog and Breed conform to Codable, you wouldn't need any additional code to encode and decode the entire graph of objects. The only difference would be how you initialize the objects:

// Given an instance of Person with a nested Dog...
let fred = Person(name: "Fred", age: 30, dog: Dog(name: "Spot", breed: .beagle))

The call to the encoder would be the same as in the previous example:

// Encode the object graph
let data = try! encoder.encode(fred)

The resulting JSON would be as follows:

{
  "name" : "Fred",
  "age" : 30,
  "dog" : {
    "name" : "Spot",
    "breed" : "Beagle"
  }
}

Decoding the JSON would reproduce the original object graph.

// Decode the object graph.
let fredsClone = try! decoder.decode(Person.self, from: data)

print(fredsClone)
// Person(name: "Fred", age: 30, dog: Dog(name: "Spot", breed: Dog.Breed.beagle))

Since Swift collection types also conform to Codable, properties than contain collections can also be encoded and decoded automatically. The following example declares a pure (i.e., non-objc) Swift class with a property, people, of type Array<Person>.

class Group: Codable {
    var title: String
    var people: [Person]

    init(title: String, people: [Person]) {
        self.title = title
        self.people = people
    }
}

// Initializing a Group
let jim = Person(name: "Jim", age: 30, dog: Dog(name: "Rover", breed: .beagle))
let sue = Person(name: "Sue", age: 27, dog: Dog(name: "Lassie", breed: .collie))
let group = Group(title: "Dog Lovers", people: [fred, sue])

// Encoding the group
let groupData = try! encoder.encode(group)

The resulting JSON would be:

{
  "title" : "Dog Lovers",
  "people" : [
    {
      "name" : "Fred",
      "age" : 30,
      "dog" : {
        "name" : "Spot",
        "breed" : "Beagle"
      }
    },
    {
      "name" : "Sue",
      "age" : 27,
      "dog" : {
        "name" : "Lassie",
        "breed" : "Collie"
      }
    }
  ]
}

Decoding would also be accomplished in the same way as the previous example:

// Decoding the group
let clonedGroup = try! decoder.decode(Group.self, from: groupData)

print(clonedGroup)
// Group(title: "Dog Lovers", people: [
//   Person(name: "Fred", age: 30, dog: Dog(name: "Spot", breed: Dog.Breed.beagle)),
//   Person(name: "Sue", age: 27, dog: Dog(name: "Lassie", breed: Dog.Breed.collie))])

Working with Dates and URLs

Date and URL conform to Codable in Swift 4, so encoding and decoding properties of those types is equally simple. For example, following struct would be the same way we encoded the struct in the previous example:

struct BlogPost: Codable {
    let title: String
    let date: Date
    let baseUrl: URL = URL(string: "https://media.aboutobjects.com/blog")!
}

let blog = BlogPost(title: "Swift 4 Coding", date: Date())
let blogData = try! encoder.encode(blog)

The resulting JSON would be:

{
  "title" : "Swift 4 Coding",
  "date" : 520269022.67031199,
  "baseUrl" : "https://media.aboutobjects.com/blog"
}

If the format of the date value above is different than what your REST API provides, you can simply set the dateEncodingStrategy property of the encoder to switch to a different format, for example ISO 8601:

encoder.dateEncodingStrategy = .iso8601

let blogData = try! encoder.encode(blog)

The JSON would now be:

{
  "title" : "Swift 4 Coding",
  "date" : "2017-06-27T15:18:26Z",
  "baseUrl" : "https://media.aboutobjects.com/blog"
}

To decode an ISO 8601 date, set the decoder's date decoding strategy:

decoder.dateDecodingStrategy = .iso8601
let clonedBlog = try! decoder.decode(BlogPost.self, from: blogData)

print(clonedBlog)
// BlogPost(title: "Swift 4 Coding", 
//          date: 2017-06-27 15:22:04 +0000,
//          baseUrl: https://media.aboutobjects.com/blog)

Using Custom Coding Keys

The coding examples we've covered so far have assumed that the keys in the JSON or plist data exactly match the names of corresponding properties. But what if they don't? For example, suppose a REST API provides the following JSON to describe a book:

{
  "title": "War of the Worlds",
  "author": "H. G. Wells",
  "publication_year": 2012,
  "number_of_pages": 240
}

Rather than use awkward property names in your Swift types, you can map any or all of the JSON or plist element names to your preferred property names. To do so, add a nested enum named CodingKeys of type String, CodingKey:

struct Book: Codable {
    var title: String
    var author: String
    var year: Int
    var pageCount: Int

    // Provide explicit string values for properties names that don't match JSON keys.
    enum CodingKeys: String, CodingKey {
        case title
        case author
        case year = "publication_year"
        case pageCount = "number_of_pages"
    }
}

Encoding and decoding will then automatically work with the provided mappings. The following example decodes a Bookfrom a literal string of JSON text:

// Swift 4 multiline string literal:
let bookJsonText =
"""
{
  "title": "War of the Worlds",
  "author": "H. G. Wells",
  "publication_year": 2012,
  "number_of_pages": 240
}
"""
let bookData = bookJsonText.data(using: .utf8)!
let book = try! decoder.decode(Book.self, from: bookData)

print(book)
// Book(title: "War of the Worlds", author: "H. G. Wells", year: 2012, pageCount: 240)

Manual (Custom) Encoding and Decoding

But what if the JSON you want to decode contains some attributes you don't need in your object model, or if your model objects have some additional properties you don't want to encode? Or perhaps you want to structure your object graph in a way that doesn't exactly match how the JSON or plist data is structured. To accommodate such situations, you can provide custom implementations of the protocol methods: encode(to encoder:) to control how your type encodes its values, and init(from decoder:) to control how it decodes its values.

Suppose, for example, a REST API provided JSON similar to the following:

let eBookJsonText = """
{
  "fileSize":5990135,
  "title":"The Old Man and the Sea",
  "author":"Ernest Hemingway",
  "releaseDate":"2002-07-25T07:00:00Z",
  "averageUserRating":4.5,
  "userRatingCount":660
}
"""

Let's assume you don't need the fileSize value in your model object, and that you'd like to model the last two elements as properties of a nested Rating object. In other words, you want your model types to look like this:

struct Rating {
    var average: Double
    var count: Int
}

struct EBook {
    var title: String
    var author: String
    var releaseDate: Date
    var rating: Rating

    enum CodingKeys: String, CodingKey {
        case title
        case author
        case releaseDate
        case averageUserRating
        case userRatingCount
    }
}

Simply adopting Codable isn't sufficient because of the mismatched data structure. To accommodate those differences, you simply provide custom implementations of the protocol. For example, to decode EBook objects from JSON, write a custom implementation of the Decodable protocol.

The implementation below first asks the decoder for a KeyedDecodingContainer object, keyed by the EBook type's coding keys. It then calls the decoding container's decode(_:forKey:) method to decode individual values as needed.

extension EBook: Decodable {
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        title = try values.decode(String.self, forKey: .title)
        author = try values.decode(String.self, forKey: .author)
        releaseDate = try values.decode(Date.self, forKey: .releaseDate)
        let average = try values.decode(Double.self, forKey: CodingKeys.averageUserRating)
        let count = try values.decode(Int.self, forKey: CodingKeys.userRatingCount)
        rating = Rating(average: average, count: count)
    }
}

Note that implementations of init(from:) are free to simply ignore any elements they're not interested in. They can also use decoded values in arbitrary ways. In the above example, the values of the averageUserRating and userRatingCountelements are used to initialize a Rating instance, which is then used to populate the rating property.

Similarly, you can make the EBook type encodable by implementing Encodable, as shown below:

extension EBook: Encodable {
    func encode(to encoder: Encoder) throws {
        var values = encoder.container(keyedBy: CodingKeys.self)
        try values.encode(title, forKey: .title)
        try values.encode(author, forKey: .author)
        try values.encode(releaseDate, forKey: .releaseDate)
        try values.encode(rating.average, forKey: CodingKeys.averageUserRating)
        try values.encode(rating.count, forKey: CodingKeys.userRatingCount)
    }
}

There are additional capabilities for enhanced customization. For example, you can populate an encoder's or decoder's userInfo dictionary with custom metadata that your implementation can use as it sees fit. There's also special handling provided for class hierarchies.

Once you've had a chance to get familiar with Swift 4 new coding features, I think you'll find them remarkably easy to use, and they should definitely help you to streamline --- or avoid writing in the first place --- a lot of otherwise tedious code.