Encoding and Decoding in Swift 4
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 Codable
without 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 Book
from 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 userRatingCount
elements 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.