Development iOS

All Things Codable

February 13, 2018

Over the last few months we have been updating many of our apps that are written in Swift from Swift 3 to Swift 4. This conversion has been much smoother than last year’s 2 to 3 conversion. Today I want to talk a little about the Swift 4 addition that I am most excited about, the introduction of the Codable protocol.

What is Codable?

Codable is actually composed of two protocols Encodable and Decodable, which provide interfaces for serializing and deserializing Swift objects.

The Encodable and Decodable protocols themselves are very simple, containing only one method each.

typealias Codable = Decodable & Encodable

protocol Encodable {
 func encode(to encoder: Encoder) throws
}

protocol Decodable {
 init(from decoder: Decoder) throws
}

Along with Codable, Swift 4 adds Encoder and Decoder protocols as interfaces for interacting with Codable objects. There are also implementations of these protocols for reading and writing to JSON in the form of JSONEncoder and JSONDecoder. As of this language version (4.0) Foundation provides implementations for JSON and PropertyList data; presumably more will be added in the future.

The rest of this post will explore ways we can convert various Swift objects to and from JSON, starting with a simple example.

Simple Case of Codable

struct Simple: Codable {
 let name: String
 let number: Int
}

In this example we have our fairly simple struct Simple. Simple’s two properties are both of types that now conform to Codable, which means when we declare conformance to Codable for Simple the compiler is able to generate the implementation of the init(from decoder:) and encode(to encoder:). We can demonstrate this by leveraging another Swift 4 feature, the multi-line string literal to create some JSON data.

let json = """
 {
 "name": "Simple Example",
 "number": 1
 }
 """.data(using: .utf8)!

Decoding an instance of Simple is as easy as passing json through an instance of JSONDecoder.

let simple = try JSONDecoder().decode(Simple.self, from: json)
dump(simple)
// ▿ Simple #1 in closure #1 (Foundation.JSONEncoder, Foundation.JSONDecoder) throws -> () in __lldb_expr_36
// - name: "Simple Example"
// - number: 1

Similarly we can convert simple back into JSON data using a JSONEncoder.

let reencoded = try JSONEncoder().encode(simple)
print(String(data: reencoded, encoding: .utf8)!)
// {
// "name" : "Simple Example",
// "number" : 1
// }

Notice that the names of the JSON keys exactly match the names of the properties in Simple. This is the default behavior when the compiler generates the entire Codable implementation for us. The next example shows how to change the keys.

Codable Keys

struct User: Codable {
 let firstName: String
 let id: Int

 enum CodingKeys: String, CodingKey {
 case firstName = "first_name"
 case id = "user_id"
 }
}

This simple User object illustrates a common situation among apps that interact with a web service. The convention for the web service may be to use snake_case for all property names whereas the client code may use camelCase by convention. Codable handles this via the CodingKeys enum nested in the User object.

CodingKeys is a “magic” type that maps a type’s property names to their encoded counterparts. All of the enum case names must exactly match the name of a property on the Codable type. The String raw value of each case is where that property will be encoded/decoded from.

let json = """
 {
 "first_name": "Johnny",
 "user_id": 11
 }
 """.data(using: .utf8)!

let user = try JSONDecoder().decode(User.self, from: json)
dump(user)
// ▿ User #1 in closure #2 (Foundation.JSONEncoder, Foundation.JSONDecoder) throws -> () in __lldb_expr_36
// - firstName: "Johnny"
// - id: 11

let reencoded = try JSONEncoder().encode(user)
print(String(data: reencoded, encoding: .utf8)!)
// {
// "first_name" : "Johnny",
// "user_id" : 11
// }

This offers a good amount of flexibility for very little code. Up to this point the compiler has been generating the implementations of Encodable and Decodable on our behalf, but what if we require more control over how our data is stored?

Codable All on Our Own

struct Message: Codable {
 static let dateFormatter: DateFormatter = {
 let formatter = DateFormatter()
 formatter.dateStyle = .short
 formatter.timeStyle = .short
 formatter.locale = Locale(identifier: "en_US")
 return formatter
 }()

 enum CodingKeys: String, CodingKey {
 case author = "author_name"
 case body = "message_content"
 case timeStamp = "time_stamp"
 }

 let author: String
 let body: String
 let timeStamp: Date

 init(from decoder: Decoder) throws {
 let container = try decoder.container(keyedBy: CodingKeys.self)
 self.author = try container.decode(String.self, forKey: .author)
 self.body = try container.decode(String.self, forKey: .body)
 let dateString = try container.decode(String.self, forKey: .timeStamp)
 if let date = Message.dateFormatter.date(from: dateString) {
 self.timeStamp = date
 } else {
 throw DecodingError.dataCorruptedError(
 forKey: CodingKeys.timeStamp,
 in: container,
 debugDescription: "Date string not formatted correctly"
 )
 }
 }

 func encode(to encoder: Encoder) throws {
 var container = encoder.container(keyedBy: CodingKeys.self)
 try container.encode(self.author, forKey: .author)
 try container.encode(self.body, forKey: .body)
 let dateString = Message.dateFormatter.string(from: self.timeStamp)
 try container.encode(dateString, forKey: .timeStamp)
 }
}

This example is quite a bit longer than the previous ones, but for good reason. We are no longer relying on the compiler to generate the Codable methods for our Message type and instead have implemented init(from decoder:) and encode(to encoder:) ourselves. Here we can see the Decoder and Encoder APIs at work. The purpose of this example is to encode/decode the time stamp field using a custom format. If the decoded date string does not match the expected format init(from decoder:) throws a DecodingError saying that the data was not the right format.

let json = """
 {
 "author_name": "Joanne",
 "message_content": "How are you doing today?",
 "time_stamp": "10/25/17, 11:21 AM"
 }
 """.data(using: .utf8)!

let message = try JSONDecoder().decode(Message.self, from: json)
dump(message)
// ▿ Message #1 in closure #3 (Foundation.JSONEncoder, Foundation.JSONDecoder) throws -> () in __lldb_expr_38
// - author: "Joanne"
// - body: "How are you doing today?"
// ▿ timeStamp: 2017-10-25 15:21:00 +0000
// - timeIntervalSinceReferenceDate: 530637660.0

let reencoded = try JSONEncoder().encode(message)
print(String(data: reencoded, encoding: .utf8)!)
// {
// "author_name" : "Joanne",
// "message_content" : "How are you doing today?",
// "time_stamp" : "10\/25\/17, 11:21 AM"
// }

It is worth noting that the Date type is Codable by default and if we wanted to use a standard date format like ISO8601 then we could have still leveraged the compiler in this case. JSONEncoder and JSONDecoder have a few options that are worth checking out for common cases like decoding dates.

Codable Enums

enum BalloonState: String, Codable {
 case deflated
 case inflated
 case popped
}

struct Balloon: Codable {
 let state: BalloonState
}

This example creates two Codable types, the enum BalloonState and the struct Balloon both of which use the compiler-generated conformance as the Simple example. The compiler is able to generate the Codable conformance for our BalloonState because we have chosen a raw type for String, which itself is Codable.

let json = """
 [{
 "state": "deflated"
 },{
 "state": "deflated"
 },{
 "state": "inflated"
 },{
 "state": "inflated"
 },{
 "state": "popped"
 },{
 "state": "deflated"
 }]
 """.data(using: .utf8)!

let balloons = try decoder.decode([Balloon].self, from: json)
print("""
 Total: \(balloons.count)
 Deflated: \(balloons.filter {$0.state == .deflated } .count)
 Inflated: \(balloons.filter {$0.state == .inflated } .count)
 Popped: \(balloons.filter {$0.state == .popped } .count)
 """
)
// Total: 6
// Deflated: 3
// Inflated: 2
// Popped: 1

An extra tidbit above is the fact that Array conforms to Codable as long as its Element type is Codable. As a bit of an aside, If we were to implement the Codable interface for our BalloonState ourself it might look something like this.

extension BalloonState {
 init(from decoder: Decoder) throws {
 let container = try decoder.singleValueContainer()
 let raw = try container.decode(String.self)
 if let value = State(rawValue: raw) {
 self = value
 } else {
 let context = DecodingError.Context(
 codingPath: decoder.codingPath,
 debugDescription: "Unexpected raw value \"\(raw)\""
 )
 throw DecodingError.typeMismatch(State.self, context)
 }
 }

 func encode(to encoder: Encoder) throws {
 var container = encoder.singleValueContainer()
 try container.encode(self.rawValue)
 }
}

Note the use of a singleValueContainer to transform the raw string value in the JSON representation into the enum case and vice versa. The next section explores this a bit more.

Codable Simple values using singleValueContainer

Codable isn’t just useful for complex types with multiple properties, it can also be used with simpler types that you want to automatically load from a JSON representation. The first example in this section show how you might load a string value in an API response into an enum.

Backward String struct

struct BackwardString: Codable {
 let value: String

 init(from decoder: Decoder) throws {
 let container = try decoder.singleValueContainer()
 let raw = try container.decode(String.self)
 self.value = String(raw.reversed())
 }

 func encode(to encoder: Encoder) throws {
 var container = encoder.singleValueContainer()
 try container.encode(self.value.reversed())
 }
}

struct SecretMessage: Codable {
 let message: BackwardString
}

let json = "{\"message\": \"esrever ni si egassem siht\"}".data(using: .utf8)!
let secret = try decoder.decode(SecretMessage.self, from: json)
dump(secret)
// ▿ SecretMessage #1 in closure #5 (Foundation.JSONEncoder, Foundation.JSONDecoder) throws -> () in __lldb_expr_38
// ▿ message: BackwardString #1 in closure #5 (Foundation.JSONEncoder, Foundation.JSONDecoder) throws -> () in __lldb_expr_38
// - value: "this message is in reverse"

While the example above is a bit contrived, I hope that the potential for useful applications is apparent. A similar approach could be used encrypting/decrypting data automatically before transferring it over a network.

Nested structures

These last two examples show two different approaches to nesting Codable objects. The first we have already seen a little bit of in the previous examples.

Nested Data, Nested Codable

struct Pagination: Codable {
 let offset: Int
 let limit: Int
 let total: Int?

 init(offset: Int, limit: Int, total: Int? = nil) {
 self.offset = offset
 self.limit = limit
 self.total = total
 }

 init(from decoder: Decoder) throws {
 let container = try decoder.container(keyedBy: CodingKeys.self)
 self.offset = try container.decode(Int.self, forKey: .offset)
 self.limit = try container.decode(Int.self, forKey: .limit)
 self.total = try container.decodeIfPresent(Int.self, forKey: .total)
 }
}

struct SearchRequest: Codable {
 enum CodingKeys: String, CodingKey {
 case term, pagination
 case isExact = "is_exact"
 }

 let term: String
 let isExact: Bool
 let pagination: Pagination
}

In this example the complex type Pagination is nested within a second complex type SearchRequest. Through the magic of Codable the pagination property encodes and decodes in the right location.

let page = Pagination(offset: 0, limit: 10)
let request = SearchRequest(term: "pikachu", isExact: true, pagination: page)
let data = try encoder.encode(request)
let string = String(data: data, encoding: .utf8)!
print(string)
// {
// "is_exact" : true,
// "pagination" : {
// "limit" : 10,
// "offset" : 0
// },
// "term" : "pikachu"
// }

let decoded = try decoder.decode(SearchRequest.self, from: data)
dump(decoded)
// ▿ SearchRequest #1 in closure #6 (Foundation.JSONEncoder, Foundation.JSONDecoder) throws -> () in __lldb_expr_62
// - term: "pikachu"
// - isExact: true
// ▿ pagination: Pagination #1 in closure #6 (Foundation.JSONEncoder, Foundation.JSONDecoder) throws -> () in __lldb_expr_62
// - offset: 0
// - limit: 10
// - total: nil

let json = """
 {
 "term": "charma",
 "is_exact": false,
 "pagination": {
 "offset": 0,
 "limit": 10,
 "total": 100
 }
 }
 """.data(using: .utf8)!

let newRequest = try decoder.decode(SearchRequest.self, from: json)
dump(newRequest)
// ▿ SearchRequest #1 in closure #6 (Foundation.JSONEncoder, Foundation.JSONDecoder) throws -> () in __lldb_expr_62
// - term: "charma"
// - isExact: false
// ▿ pagination: Pagination #1 in closure #6 (Foundation.JSONEncoder, Foundation.JSONDecoder) throws -> () in __lldb_expr_62
// - offset: 0
// - limit: 10
// ▿ total: Optional(100)
// - some: 100

Flat Data, Nested Codable

struct Pagination: Codable {
 enum CodingKeys: String, CodingKey {
 case offset = "PageOffset"
 case limit = "NumberPerPage"
 }

 let offset: Int
 let limit: Int
}

struct SearchRequest: Codable {
 enum CodingKeys: String, CodingKey {
 case term
 }

 let term: String
 let pagination: Pagination

 init(term: String, pagination: Pagination) {
 self.term = term
 self.pagination = pagination
 }

 init(from decoder: Decoder) throws {
 let container = try decoder.container(keyedBy: CodingKeys.self)
 self.term = try container.decode(String.self, forKey: .term)
 self.pagination = try Pagination(from: decoder)
 }

 func encode(to encoder: Encoder) throws {
 var container = encoder.container(keyedBy: CodingKeys.self)
 try container.encode(self.term, forKey: .term)
 try self.pagination.encode(to: encoder)
 }
}

In this example the Pagination properties are intermingled in with the SearchRequest properties in the serialized form. Programmatically, it may be more convenient to deal with the search information and the pagination information separately.

In this case the SearchRequest constructor passes the same decoder instance to the Pagination constructor, likewise for the encode(to encoder:) method (rather than creating a nested container with container(keyedBy:)).

let request = SearchRequest(term: "dunsparce", pagination: Pagination(offset: 0, limit: 20))
let data = try encoder.encode(request)
let string = String(data: data, encoding: .utf8)!
print(string)
// {
// "NumberPerPage" : 20,
// "PageOffset" : 0,
// "term" : "dunsparce"
// }

let json = """
 {
 "PageOffset": 0,
 "NumberPerPage": 30,
 "term": "tropius"
 }
 """.data(using: .utf8)!

let newRequest = try decoder.decode(SearchRequest.self, from: json)
dump(newRequest)
// ▿ SearchRequest #1 in closure #7 (Foundation.JSONEncoder, Foundation.JSONDecoder) throws -> () in __lldb_expr_62
// - term: "tropius"
// ▿ pagination: Pagination #1 in closure #7 (Foundation.JSONEncoder, Foundation.JSONDecoder) throws -> () in __lldb_expr_62
// - offset: 0
// - limit: 30

Codable Moving Forward

I hope this has provided insight into the simplicity and flexibility of the new Codable protocol in Swift 4. I am curious to see how it can take even more complicated scenarios that may come up, especially when integrating with legacy APIs.

You can find a Swift playground with these examples on github. For more information on Codable and Swift 4 check out What’s New in Swift 4? and Swift 4 Decodable: Beyond The Basics.

Jeff Kloosterman
Jeff Kloosterman
Head of Client Services

Stay in the loop with our latest content!

Select the topics you’re interested to receive our new relevant content in your inbox. Don’t worry, we won’t spam you.

Michigan Software Labs joins the Inc. 5000 list of fastest growing companies in the U.S.
Press Release

Michigan Software Labs joins the Inc. 5000 list of fastest growing companies in the U.S.

August 12, 2020

Michigan Software Labs has earned its first recognition in Inc. magazine’s influential Inc. 5000 list. The list represents a unique look at the most successful companies within the American economy’s most dynamic segment—its independent small businesses.

Read more
Clutch Names Michigan Software Labs as a 2019 Top Developer in U.S.A.
Business Team

Clutch Names Michigan Software Labs as a 2019 Top Developer in U.S.A.

November 14, 2019

Clutch is a B2B research, ratings, and reviews firm located in downtown Washington, D.C.. Clutch connects businesses with the best-fit agencies, software, and consultants they need to tackle business challenges together and with confidence. Clutch’s methodology compares business service providers and software in a specific market based on verified client reviews, services offered, work quality, and market presences.

Read more
MichiganLabs to double workforce as part of expansion
Team Press Release

MichiganLabs to double workforce as part of expansion

October 10, 2019

“As our clients continue to grow and evolve, we intend to serve them best by growing along with them,” explains Michigan Software Labs co-founder and managing partner, Mark Johnson. “Organizations are increasingly seeing how technology can serve their customers and grow their businesses. Developing the right custom software gives companies a clear market advantage.”

Read more
View more articles