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.