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.
Looking for more like this?
Sign up for our monthly newsletter to receive helpful articles, case studies, and stories from our team.
Between the brackets: MichiganLabs’ approach to software development
February 12, 2024 Read moreHow to Bring Order to the Chaos of Job Applications
August 8, 2023We discuss some tips & tricks for the job application process
Read more