iOS
Swift

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

先日はじめて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"]!
semaphore.signal()
} catch let e {
print(e)
}
}
task.resume()
_ = semaphore.wait(timeout: DispatchTime.distantFuture)
return returnnumberOfPhotos
}


うん、汚いですね。

可読性めちゃくちゃ悪いな……とは当時から思ってました。

semaphoreで強引に同期処理にさせてるのもよくないみたいですが、そこは別件としてください。

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()
}
}
}


相変わらずsemaphoreで同期処理させてますが、それはお気になさらず。

関数の呼び出し元で検索クエリを入れてもらい、該当するユーザーIDと先頭が一致するユーザーの情報が複数APIから返ってきます。

最初素人考えで、複数返してくるのだから、ループ文書かないとダメなのか? と思っていましたが、

構造体でItemをネストした構造体として、Itemsという配列の一要素にしているので、

上記のコードで上手いことやってくれます。

Codableすごい!

無理やりキャストしていた例だと、必要な値にたどりつくまで、

地獄みたいな段階を踏んでいましたが、この関数で返しているUserを使うと、シンプルに書けます。


構造体から値を抜き出す

class ViewModel {

var logins = [String]()
//~~~~~~~中略~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
func setUserInformation(user: User) -> () {
logins = user.items.map { $0.login }
}