なぜモバイルエンジニアはJSONのキーを変えたりnullにしないで欲しいとか言うのか?

35258 ワード

この記事は?

あなたはAPI設計の話し合いをしたことがありますか?もしあなたがAPI通信でJSONを使った機会があればこんなことを言われたことはないでしょうか?

JSONのキーを変わってない?

この値nullで返して欲しくないんだけど。。。

そんなことを言われた時に読む記事です

JSONとは?

まずはJSONが何なのかをみていきましょう

公式サイトに書かれていることがほぼ全てなのですが、下記の2点が特徴です。

  • 名前/値のペアの集まり
  • 値の順序付きリスト。
 (JavaScript Object Notation)は、軽量のデータ交換フォーマットです。人間にとって読み書きが容易で、マシンにとっても簡単にパースや生成を行なえる形式です。
- 名前/値のペアの集まり。様々な言語で、これは*オブジェクト*、レコード、構造体、ディクショナリ、ハッシュテーブル、キーのあるリスト、連想配列として実現されています。
- 値の順序付きリスト。ほとんどの言語で、これは*配列*として実現されています。

JSON

また今回のようにSwiftなどの静的型付け言語を書く場合の比較としてJSONはスキーマレスであるということがあります。どういうことなのか次で詳しくみていきましょう。

SwiftでJSONを認識するには?

SwiftでJSONを認識するにはデコード作業を行う必要があります。
SwiftでJSONをデコードする方法はいくつかありますが今回は公式のやり方である Codable の方法を見ていきましょう。

実際のコードはこちらです。

struct PlayersResponse: Codable {
   
    var players: [Player]
    
    enum CodingKeys: String, CodingKey {
      case players = "players"
    }
}

struct Player: Codable {
    
    // 値の定義
    var name: String
    var birthday: Date
    var previousClub: String
    
    // JSONとどのようにマッピングするか?
    enum CodingKeys: String, CodingKey {
        case name = "name"
        case birthday = "birthday"
        case previousClub = "previous_club"
    }
}

extension Player {
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        name = try container.decode(String.self, forKey: .name)
        previousClub = try container.decode(String.self, forKey: .previousClub)

        let formatter = DateFormatter.yyyyMMdd
        let birthdayString = try? container.decode(String.self, forKey: .birthday)
        if let birthdayString = birthdayString,let birthday = formatter.date(from: birthdayString)  {
            self.birthday = birthday
        }else{
            fatalError()
        }      
    }
}

extension DateFormatter {
    // 型の定義が必要
    convenience init(format: String) {
        self.init()
        dateFormat = format
        locale = Locale(identifier: "ja_JP")
    }
    
    static let yyyyMMdd: DateFormatter = {
      let formatter = DateFormatter()
      formatter.dateFormat = "yyyy-MM-dd"
      formatter.calendar = Calendar(identifier: .iso8601)
      formatter.timeZone = TimeZone(secondsFromGMT: 0)
      formatter.locale = Locale(identifier: "en_US_POSIX")
      return formatter
    }()
}

let data = json.data(using: .utf8)
playersResponse = try! JSONDecoder().decode(PlayersResponse.self, from: data!)

まず初めにstructでCodableの定義をしてあげましょう。

ここではJSONの形がわかるようにマッピングの細かい方法を書きます。

基本的には下記のようにSwiftで用意されている型と名称を設定するとマッピングすることができます。

    var name: String

ただし、特定の型付けをしたい場合は下記のようなマッピング処理を行い、JSONできた値を正しくSwiftで認識できるようにする必要があります。

今回の例では birthday を年月日の形式で認識したいためこのようにマッピングしています


let formatter = DateFormatter.yyyyMMdd
let birthdayString = try? container.decode(String.self, forKey: .birthday)
if let birthdayString = birthdayString,let birthday = formatter.date(from: birthdayString)  {
    self.birthday = birthday
}else{
    fatalError()
}    

~~~
    static let yyyyMMdd: DateFormatter = {
      let formatter = DateFormatter()
      formatter.dateFormat = "yyyy-MM-dd"
      formatter.calendar = Calendar(identifier: .iso8601)
      formatter.timeZone = TimeZone(secondsFromGMT: 0)
      formatter.locale = Locale(identifier: "en_US_POSIX")
      return formatter
    }()

Apple Developer Documentation

decodeした値を実際に表示させてみる

SwiftにはPlayGroundという機能がありここで実際にSwiftを書いてビルドすることができます試してみましょう。

下記のコードをそのままコピペしてPlayGround上で実行してみてください


let json = """
{
  "players": [
    {
      "name": "サディオ・マネ",
      "birthday": "1992-04-10",
      "previous_club": "サウサンプトン"
    },
    {
      "name": "ロベルト・フィルミノ",
      "birthday": "1991-10-02",
      "previous_club": "ホッフェンハイム"
    },
    {
      "name": "モハメド・サラー",
      "birthday": "1992-04-10",
      "previous_club": "ローマ"
    }
  ]
}
"""

struct PlayersResponse: Codable {
   
    var players: [Player]
    
    enum CodingKeys: String, CodingKey {
      case players = "players"
    }
}

struct Player: Codable {
    
    var name: String
    var birthday: Date
    var previousClub: String
    
    // json上でどうか?
    enum CodingKeys: String, CodingKey {
        case name = "name"
        case birthday = "birthday"
        case previousClub = "previous_club"
    }

}

extension DateFormatter {

    // 型の定義が必要
    convenience init(format: String) {
        self.init()
        dateFormat = format
        locale = Locale(identifier: "ja_JP")
    }
    
    static let yyyyMMdd: DateFormatter = {
      let formatter = DateFormatter()
      formatter.dateFormat = "yyyy-MM-dd"
      formatter.calendar = Calendar(identifier: .iso8601)
      formatter.timeZone = TimeZone(secondsFromGMT: 0)
      formatter.locale = Locale(identifier: "en_US_POSIX")
      return formatter
    }()
}

extension Player {
    // jsonはデコードする必要があるよ!
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        name = try container.decode(String.self, forKey: .name)
        previousClub = try container.decode(String.self, forKey: .previousClub)

        let formatter = DateFormatter.yyyyMMdd
        let birthdayString = try? container.decode(String.self, forKey: .birthday)
        if let birthdayString = birthdayString,let birthday = formatter.date(from: birthdayString)  {
            self.birthday = birthday
        }else{
            fatalError()
        }   
    }
}

import UIKit
import PlaygroundSupport

// 表示
class MyViewController : UIViewController, UITableViewDataSource {

    var playersResponse:PlayersResponse?
    
    override func loadView() {
     
        let view = UIView()
        view.backgroundColor = .white

        // tableviewの設定
        let tableView = UITableView()
        tableView.frame = CGRect(x: 0, y: 0, width: 350, height: 200)
        tableView.dataSource = self

        
        // デコード処理
        let data = json.data(using: .utf8)
        playersResponse = try! JSONDecoder().decode(PlayersResponse.self, from: data!)
        tableView.reloadData()
          
          view.addSubview(tableView)
          self.view = view
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        
        return playersResponse?.players.count ?? 0
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = UITableViewCell()
				cell.textLabel?.text = playersResponse?.players[indexPath.row].name
        return cell
    }
}

PlaygroundPage.current.liveView = MyViewController()

するとこのように表示されます。

このようにデコードした値を表示することができます。

ではここで本題の「なぜモバイルエンジニアはJSONのキーを変えたりnullにしないで欲しいとか言うのか?」をみていきましょう。

実際に意地悪をして「"name": "サディオ・マネ",」の行を削除してみましょう。どうなるでしょうか?

let json = """
{
  "players": [
    {

      "birthday": "1992-04-10",
      "previous_club": "サウサンプトン"
    },
    {
      "name": "ロベルト・フィルミノ",
      "birthday": "1991-10-02",
      "previous_club": "ホッフェンハイム"
    },
    {
      "name": "モハメド・サラー",
      "birthday": "1992-04-10",
      "previous_club": "ローマ"
    }
  ]
}
"""

エラーになってしまいますね。

Swift.DecodingError.keyNotFoundというエラーは文字通り定義されているキーが見つからないということを示しています。

__lldb_expr_9/MyPlayground.playground:132: Fatal error: 'try!' expression unexpectedly raised an error: Swift.DecodingError.keyNotFound(CodingKeys(stringValue: "name", intValue: nil), Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "players", intValue: nil), _JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key CodingKeys(stringValue: \"name\", intValue: nil) (\"name\").", underlyingError: nil))

この場合はnameをOptionalで定義するなどの回避策が必要です。

このようにSwiftは静的型付け言語であり明示的にJSONのマッピングを指定する必要があります。

このようなことを起こさないためにAPI設計をしてサーバーサイドとフロント側の仕様を合わせることはとても重要です。

まとめ

今回は最小限のコードで「なぜフロントエンドはJSONのキーを変えたりnullにしないで欲しいとか言うのか?」について説明できるようにコードを書いてみました。

ご質問やこんな例の方がいいんじゃないか?など意見ありましたらお気軽にコメントください