Swift
Decodable
Codable

【Swift】JSON内にある様々なIDを型安全に扱えるようDecodableでdecodeする

この記事はニフティグループ Advent Calendar 2018の13日目の記事です。


TL;DR

iOSアプリではWEB APIやローカルのファイル等、外部から情報を取得して画面に表示することが多いです。

それらの情報の中には多くのIDが含まれています。

IDをキーにしてローカルにキャッシュしたり、IDをキーにして検索を実行したり……

さまざまな場面で利用されているIDを型安全に扱う方法として、IDをstructで表現する方法が有名だと思います。

本記事では以下を紹介します。


  • IDを型安全に扱う方法のご紹介

  • JSON内のIDを型安全に扱えるようdecodeする方法


IDを型安全に扱う方法

GitHubのAPIを参考にご説明します。※説明の都合上、簡略化しています。

以下のようなリポジトリ検索のリクエストを送信した場合のレスポンスはこちらです。

https://api.github.com/search/repositories?q=swift

{

"total_count": 127415,
"incomplete_results": false,
"items": [
{
"id": 44838949, # リポジトリのID
"full_name": "apple/swift",
"owner": {
"login": "apple",
"id": 10639145 # ユーザのID
}
},
{
"id": 27398751,
"full_name": "btrn/Swift",
"owner": {
"login": "btrn",
"id": 8502419
}
}
]
}

このレスポンスの中にIDが2種類以上存在することに気づきましたか?

それぞれ、リポジトリのIDとユーザのIDで、意味が異なるIDです。

こちらをそのままJSONに書かれている通りにSwiftの実装におこすと以下のようになります。


Repository.swift

struct Repository {

let id: Int
let fullName: String
let owner: User
}


User.swift

struct User {

let id: Int
let login: String
}

愚直に実装するとどちらのIDもIntで表現することになると思います。

ですが、以下のような場合にバグになってしまう可能性があります。

項目
情報

実装したい機能
リポジトリのお気に入り

必要な情報
リポジトリのID(Int

ミス
ユーザのID(Int)を保存してしまった

原因
同じ Int を扱うのでコンパイル時に気づけなかった

そこで以下のようにIDをstructで表現することによって型安全にすることができます。


Repository.swift

struct Repository {

let id: ID
let fullName: String
let owner: User

struct ID {
let rawValue: Int
}
}



User.swift

struct User: Decodable {

let id: ID
let login: String

struct ID {
let rawValue: Int
}
}


それぞれRepository.ID型とUser.ID型の別々の型になり、間違って実装したらコンパイルエラーが発生するようになりました!!💪

// IDをIntで扱うとリポジトリのIDでもユーザのIDでも、どちらでも使える

func save(from id: Int)

// リポジトリのID以外は受け取れない
func save(from id: Repository.ID)


JSON内のIDを型安全に扱えるようdecodeする方法

ここまででIDを型安全に扱う方法をご紹介してきました。

では、実際にGitHubのAPIのレスポンスをDecodableプロトコルを利用してそれぞれの型に落としていくことを考えたいと思います。


Decodableプロトコルに準拠


Repository.swift

struct Repository: Decodable {

...
struct ID: Decodable {
let rawValue: Int
}
}


User.swift

struct User: Decodable {

...
struct ID: Decodable {
let rawValue: Int
}
}

コンパイルエラーはでなくなりますが、structで型のネストをしてしまったので、実行時に落ちてしまいます。

現状は以下のようなレスポンスを期待していると解釈されてしまいます。


期待しているレスポンス(抜粋)

    {

"id": { # 型のネスト
"rawValue": 44838949 # リポジトリのID
},
"full_name": "apple/swift",
"owner": {
"login": "apple",
"id": { # 型のネスト
"rawValue": 10639145 # ユーザのID
}
}
}


型のネストを解決

これを先程のレスポンスをデコードできるようにしつつ、型安全を保つには

RawRepresentableプロトコルに準拠させます

https://developer.apple.com/documentation/swift/rawrepresentable

enumがIntやStringを継承すると暗黙的にrawValueというプロパティが出現するのはこのプロトコルのおかげです。

最終的なコードはこのようになります。


Repository.swift

struct Repository: Decodable {

let id: ID
let fullName: String
let owner: User

struct ID: RawRepresentable, Decodable {
let rawValue: Int
}
}



User.swift

struct User: Decodable {

let id: ID
let login: String

struct ID: RawRepresentable, Decodable {
let rawValue: Int
}
}


let data = """

[
{
"
id": 44838949,
"
full_name": "apple/swift",
"
owner": {
"
login": "apple",
"
id": 10639145
}
},
{
"
id": 27398751,
"
full_name": "btrn/Swift",
"
owner": {
"
login": "btrn",
"
id": 8502419
}
}
]
"""
.data(using: .utf8)!

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let repositories = try! decoder.decode([Repository].self, from: data)


まとめ


  • iOSアプリではアプリ外からも多くの情報を取得して表示している

  • それらの情報にはさまざまなIDが含まれている

  • IDは取り間違えるとバグになるため、structを用いて別々の型にすることで、コンパイラによるバグの検出が可能


  • Decodableプロトコルを用いてstruct化したIDをdecodeするにはIDの型をRawRepresentableプロトコルに準拠させる


最後に

明日は@5_21maimaiさんが「5年もののiOSアプリで、ついに自動テストを開始した話」をしてくれます!お楽しみに!!