Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

この記事はニフティグループ 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アプリで、ついに自動テストを開始した話」をしてくれます!お楽しみに!!

hicka04
東京でiOSアプリメインで開発してます
nifty
インターネット接続やブログといったインターネット関連サービスを開発・提供している企業です。
http://www.nifty.co.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした