All Things Codable
February 13, 2018Over 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.
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.
August 12, 2020Michigan 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.
November 14, 2019Clutch 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
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