LoginSignup
10
9

More than 3 years have passed since last update.

APIから受け取ったJSONデータをCodableプロトコルで構造体にマッピングする

Last updated at Posted at 2019-04-20

先日はじめてJSONデータをちゃんとパースする書き方をしたので、まとめます。

構造体を使わずに無理やりキャストする例

Codableプロトコルを使わなくても、やる方法はあります。
相当な力技なんですが、二ヶ月前くらいに処理したときはこれでやりました。

Swift3でJSONパースを行う
とかを参考にして、無理やりキャストしたのが、下記です。

JSONSerializationでやった例
func getNumberOfPhotos(url: URL) -> Int {

        var request = URLRequest(url: url)
        var returnnumberOfPhotos = 0
        let semaphore = DispatchSemaphore(value: 0)
        request.httpMethod = "GET"

        let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
            guard let data = data else { return }
            do {
                let dictinaryForJSON = try JSONSerialization.jsonObject(with: data, options: []) as! Dictionary<String, Any>
                let dictionaryForJSONBydata = dictinaryForJSON["data"]! as! Dictionary<String, Any>
                let dictionaryForJSONBydataBycounts = dictionaryForJSONBydata["counts"]! as! Dictionary<String, Int>
                returnnumberOfPhotos = dictionaryForJSONBydataBycounts["media"]!
            } catch let e {
                print(e)
            }
        }
        task.resume()
        return returnnumberOfPhotos
    }

うん、汚いですね。
可読性めちゃくちゃ悪いな……とは当時から思ってました。

InstagramのAPIが返してくる、下記のJSONデータをparseしたかったのが目的でした。

{
  "data": {
    "id": "1574083",
    "username": "snoopdogg",
    "full_name": "Snoop Dogg",
    "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1574083_75sq_1295469061.jpg",
    "bio": "This is my bio",
    "website": "http://snoopdogg.com",
    "is_business": false,
    "counts": {
      "media": 1320,
      "follows": 420,
      "followed_by": 3410
    }
  }
}

User Endpoints

初心者過ぎて構造体のマッピングの方法がわからずに、こんなやり方をしましたが、この方法をとるメリットはないですね。

構造体にマッピングする例

APIと通信する際は、アプリ側でも受け取る構造体を定義しましょう。
Githubのユーザー検索APIだと、

{
  "total_count": 12,
  "incomplete_results": false,
  "items": [
    {
      "login": "mojombo",
      "id": 1,
      "node_id": "MDQ6VXNlcjE=",
      "avatar_url": "https://secure.gravatar.com/avatar/25c7c18223fb42a4c6ae1c8db6f50f9b?d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-user-420.png",
      "gravatar_id": "",
      "url": "https://api.github.com/users/mojombo",
      "html_url": "https://github.com/mojombo",
      "followers_url": "https://api.github.com/users/mojombo/followers",
      "subscriptions_url": "https://api.github.com/users/mojombo/subscriptions",
      "organizations_url": "https://api.github.com/users/mojombo/orgs",
      "repos_url": "https://api.github.com/users/mojombo/repos",
      "received_events_url": "https://api.github.com/users/mojombo/received_events",
      "type": "User",
      "score": 105.47857
    }
  ]
}

こんなJSONオブジェクトを返してきます。
これをSwiftで書いてやると、

struct User: Codable {
    let total_count: Int
    let incomplete_results: Bool
    let items: [Item]

    struct Item: Codable {
        let login: String
        let id: Int
        let node_id: String
        let avatar_url: URL
        let gravatar_id: String?
        let url: URL
        let html_url: URL
        let followers_url: URL
        let subscriptions_url: URL
        let organizations_url: URL
        let repos_url: URL
        let received_events_url: URL
        let type: String
        let score: Double
    }
}

こんな感じになります。
Codableプロトコルに準拠させるのがポイントです。
もしもiOSアプリで必要な情報が限られて、絞りたいのであれば、
構造体の中でその変数だけ用意してやればOKです。
ただし、構造体で定義した変数がAPIから返されたデータの中に入っていないと、parseに失敗します。

Codableでやった例
    func searchGithubUser(query: String) -> User {

        let url = URL(string: "https://api.github.com/search/users?q=" + query)!
        var request = URLRequest(url: url)
        var rowData = Data()
        let semaphore = DispatchSemaphore(value: 0)
        let decoder: JSONDecoder = JSONDecoder()
        request.httpMethod = "GET"

        let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
            guard let data = data else { return }
            rowData = data
            semaphore.signal()
        }
        task.resume()
        _ = semaphore.wait(timeout: DispatchTime.distantFuture)

        do {
            let user: User = try decoder.decode(User.self, from: rowData)
            return user
        } catch let e {
            print("JSON Decode Error :\(e)")
            fatalError()
        }
    }

関数の呼び出し元で検索クエリを入れてもらい、該当するユーザーIDと先頭が一致するユーザーの情報が複数APIから返ってきます。
最初素人考えで、複数返してくるのだから、ループ文書かないとダメなのか? と思っていましたが、
構造体でItemをネストした構造体として、Itemsという配列の一要素にしているので、
上記のコードで上手いことやってくれます。
Codableすごい!

無理やりキャストしていた例だと、必要な値にたどりつくまで、
地獄みたいな段階を踏んでいましたが、この関数で返しているUserを使うと、シンプルに書けます。

構造体から値を抜き出す
class ViewModel {
    var logins = [String]()
    //~~~~~~~中略~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    func setUserInformation(user: User) -> () {
        logins = user.items.map { $0.login }
    }
10
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
9