The Beauty of Swift 4's Codable Protocol
July 13, 2017
Swift 4’s Codable
protocol makes JSON a first-class citizen in Swift.
By adding Codable
to a struct or class you can automagically encode its properties as JSON or create it by decoding from JSON. For example, consider a struct that describes a Ford Taurus:
struct Car: Codable {
var make: String
var model: String
}
let taurus = Car(make: "Ford", model: "Taurus")
Converting to and from JSON
To encode the Car struct as JSON, first create a JSONEncoder
and then encode the struct.
var json: String?
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(taurus) {
if let jsonString = String(data: encoded, encoding: .utf8) {
print(jsonString)
json = jsonString
}
}
The resulting JSON looks like {"make":"Ford","model":"Taurus"}
, which we can convert back into a Car struct.
let decoder = JSONDecoder()
if let jsonString = json {
let jsonData = jsonString.data(using: .utf8)!
if let decoded = try? decoder.decode(Car.self, from: jsonData) {
print("Make: \(decoded.make)")
print("Model: \(decoded.model)\n")
}
}
// prints out:
// Make: Ford
// Model: Taurus
But this isn’t the most exciting feature.
Interacting with REST APIs
Let’s say you’re working on a GitHub app and wanted to access the repos of a given language. You don’t want to know every detail about the repo, just the repo’s name, owner, URL, and number of stars, so you set up a struct for an Owner and one for a repo:
struct Owner: Codable {
var login: String
var url: String
}
struct Repository: Codable {
var name: String
var full_name: String
var owner: Owner
var url: String
var stargazers_count: Int
var language: String
}
The relevant endpoint of GitHub’s public API is “https://api.github.com/search/repositories” and we’re going to sort the returned repos in descending order of stars. For example, if you wanted the Swift repos sorted by stars, you’d use “https://api.github.com/search/repositories?q=language:swift&sort=stars&order=desc”. Enter the URL in your browser search bar and see what you get. The response from GitHub’s server looks like this:
{
"total_count": 284186,
"incomplete_results": false,
"items": [repo_1, repo_2, …]
}
where each repo in the items array contains fields that look like those in the Repository
struct defined above and many more. For example, at the time of writing, the first repo in the results is for Alamofire. It looks like this:
{
"id": 22458259,
"name": "Alamofire",
"full_name": "Alamofire/Alamofire",
"owner": {
"login": "Alamofire",
"id": 7774181,
"avatar_url": "https://avatars0.githubusercontent.com/u/7774181?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/Alamofire",
"html_url": "https://github.com/Alamofire",
"followers_url": "https://api.github.com/users/Alamofire/followers",
"following_url": "https://api.github.com/users/Alamofire/following{/other_user}",
"gists_url": "https://api.github.com/users/Alamofire/gists{/gist_id}",
"starred_url": "https://api.github.com/users/Alamofire/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/Alamofire/subscriptions",
"organizations_url": "https://api.github.com/users/Alamofire/orgs",
"repos_url": "https://api.github.com/users/Alamofire/repos",
"events_url": "https://api.github.com/users/Alamofire/events{/privacy}",
"received_events_url": "https://api.github.com/users/Alamofire/received_events",
"type": "Organization",
"site_admin": false
},
"private": false,
"html_url": "https://github.com/Alamofire/Alamofire",
"description": "Elegant HTTP Networking in Swift",
"fork": false,
"url": "https://api.github.com/repos/Alamofire/Alamofire",
"forks_url": "https://api.github.com/repos/Alamofire/Alamofire/forks",
"keys_url": "https://api.github.com/repos/Alamofire/Alamofire/keys{/key_id}",
"collaborators_url": "https://api.github.com/repos/Alamofire/Alamofire/collaborators{/collaborator}",
"teams_url": "https://api.github.com/repos/Alamofire/Alamofire/teams",
"hooks_url": "https://api.github.com/repos/Alamofire/Alamofire/hooks",
"issue_events_url": "https://api.github.com/repos/Alamofire/Alamofire/issues/events{/number}",
"events_url": "https://api.github.com/repos/Alamofire/Alamofire/events",
"assignees_url": "https://api.github.com/repos/Alamofire/Alamofire/assignees{/user}",
"branches_url": "https://api.github.com/repos/Alamofire/Alamofire/branches{/branch}",
"tags_url": "https://api.github.com/repos/Alamofire/Alamofire/tags",
"blobs_url": "https://api.github.com/repos/Alamofire/Alamofire/git/blobs{/sha}",
"git_tags_url": "https://api.github.com/repos/Alamofire/Alamofire/git/tags{/sha}",
"git_refs_url": "https://api.github.com/repos/Alamofire/Alamofire/git/refs{/sha}",
"trees_url": "https://api.github.com/repos/Alamofire/Alamofire/git/trees{/sha}",
"statuses_url": "https://api.github.com/repos/Alamofire/Alamofire/statuses/{sha}",
"languages_url": "https://api.github.com/repos/Alamofire/Alamofire/languages",
"stargazers_url": "https://api.github.com/repos/Alamofire/Alamofire/stargazers",
"contributors_url": "https://api.github.com/repos/Alamofire/Alamofire/contributors",
"subscribers_url": "https://api.github.com/repos/Alamofire/Alamofire/subscribers",
"subscription_url": "https://api.github.com/repos/Alamofire/Alamofire/subscription",
"commits_url": "https://api.github.com/repos/Alamofire/Alamofire/commits{/sha}",
"git_commits_url": "https://api.github.com/repos/Alamofire/Alamofire/git/commits{/sha}",
"comments_url": "https://api.github.com/repos/Alamofire/Alamofire/comments{/number}",
"issue_comment_url": "https://api.github.com/repos/Alamofire/Alamofire/issues/comments{/number}",
"contents_url": "https://api.github.com/repos/Alamofire/Alamofire/contents/{+path}",
"compare_url": "https://api.github.com/repos/Alamofire/Alamofire/compare/{base}...{head}",
"merges_url": "https://api.github.com/repos/Alamofire/Alamofire/merges",
"archive_url": "https://api.github.com/repos/Alamofire/Alamofire/{archive_format}{/ref}",
"downloads_url": "https://api.github.com/repos/Alamofire/Alamofire/downloads",
"issues_url": "https://api.github.com/repos/Alamofire/Alamofire/issues{/number}",
"pulls_url": "https://api.github.com/repos/Alamofire/Alamofire/pulls{/number}",
"milestones_url": "https://api.github.com/repos/Alamofire/Alamofire/milestones{/number}",
"notifications_url": "https://api.github.com/repos/Alamofire/Alamofire/notifications{?since,all,participating}",
"labels_url": "https://api.github.com/repos/Alamofire/Alamofire/labels{/name}",
"releases_url": "https://api.github.com/repos/Alamofire/Alamofire/releases{/id}",
"deployments_url": "https://api.github.com/repos/Alamofire/Alamofire/deployments",
"created_at": "2014-07-31T05:56:19Z",
"updated_at": "2017-07-13T15:42:37Z",
"pushed_at": "2017-07-10T17:53:01Z",
"git_url": "git://github.com/Alamofire/Alamofire.git",
"ssh_url": "git@github.com:Alamofire/Alamofire.git",
"clone_url": "https://github.com/Alamofire/Alamofire.git",
"svn_url": "https://github.com/Alamofire/Alamofire",
"homepage": "",
"size": 2814,
"stargazers_count": 24353,
"watchers_count": 24353,
"language": "Swift",
"has_issues": true,
"has_projects": true,
"has_downloads": true,
"has_wiki": false,
"has_pages": false,
"forks_count": 4233,
"mirror_url": null,
"open_issues_count": 11,
"forks": 4233,
"open_issues": 11,
"watchers": 24353,
"default_branch": "master",
"score": 1.0
}
But we only want the fields defined in Repository
. We don’t want to handle all the fields shown above.
One of the great things about Codable is that you can simply decode the fields that you care about and can ignore the rest. One thing we can do is make a struct for the GitHub response
struct GithubResponse: Codable {
var total_count: Int
var incomplete_results: Bool
var items: [Repository]
}
and another to submit the query and decode the response.
struct GHReposForLanguage {
private let baseUrl = "https://api.github.com/search/repositories"
private let starsQuery = "sort=stars&order=desc"
let language: String
private let encoder = JSONEncoder()
private let decoder = JSONDecoder()
init(language: String) {
self.language = language
}
var url: String {
return "\(baseUrl)?q=language:\(language)&\(starsQuery)"
}
func getMostStarredRepos(maxCount: Int) -> ArraySlice? {
if let queryUrl = URL(string: self.url) {
if let data = try? Data(contentsOf: queryUrl) {
if let response = try? decoder.decode(GithubResponse.self, from: data) {
return response.items[0..<maxCount]
}
}
}
return nil
}
}
One thing that you’ll notice is that we’re decoding the response into a GithubResponse
, where the items array contains Repository
structs, which are themselves Codable
. Swift 4 handles nested Codable
properties with ease! This is another excellent property of the Codable
protocol that makes it a joy to code in Swift.
We can put this together into a simple function to print the top repos of a given language.
func printTopRepos(of language: String, maxCount: Int = 10) {
let query = GHReposForLanguage(language: language)
if let topRepos = query.getMostStarredRepos(maxCount: 10) {
print("Language: \(language)\n")
for repo in topRepos {
print(repo.name)
}
print("\n")
}
}
printTopRepos(of: "clojure")
printTopRepos(of: "swift")
printTopRepos(of: "scala")
The results, for Clojure, Scala, and Swift, are shown below.
Clojure
FiraCode
LightTable
clojurescript
om
leiningen
overtone
icepick
riemann
compojure
clojure-koans
Swift
Alamofire
awesome-ios
ReactiveCocoa
Charts
SwiftyJSON
open-source-ios-apps
swift-algorithm-club
awesome-swift
SwiftGuide
Perfect
Scala
spark
incubator-predictionio
playframework
scala
shadowsocks-android
akka
gitbucket
finagle
ArnoldC
aerosolve
Swift playgrounds are at https://github.com/klgraham/json-with-swift4.
For the nitty-gritty details of using the Codable
protocol, Ben Scheirman has written an excellent guide to JSON in Swift 4.