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

Looking for more like this?

Sign up for our monthly newsletter to receive helpful articles, case studies, and stories from our team.

User research: The heartbeat of successful development
Business Design Process

User research: The heartbeat of successful development

July 15, 2024

User research in software development is essential for success. Learn how consistently engaging in methods like user interviews, usability testing, and field studies throughout the product lifecycle, helps ensure your solutions align closely with user needs.

Read more
How to Prepare for our Associate Software Developer Position
Team

How to Prepare for our Associate Software Developer Position

June 30, 2023

Tips for applying to MichiganLab's Associate Software Developer program

Read more
3 tips for navigating tech anxiety as an executive
Business

3 tips for navigating tech anxiety as an executive

March 13, 2024

C-suite leaders feel the pressure to increase the tempo of their digital transformations, but feel anxiety from cybersecurity, artificial intelligence, and challenging economic, global, and political conditions. Discover how to work through this.

Read more
View more articles