質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.35%
JSON

JSON(JavaScript Object Notation)は軽量なデータ記述言語の1つである。構文はJavaScriptをベースとしていますが、JavaScriptに限定されたものではなく、様々なソフトウェアやプログラミング言語間におけるデータの受け渡しが行えるように設計されています。

Swift

Swiftは、アップルのiOSおよびOS Xのためのプログラミング言語で、Objective-CやObjective-C++と共存することが意図されています

API

APIはApplication Programming Interfaceの略です。APIはプログラムにリクエストされるサービスがどのように動作するかを、デベロッパーが定めたものです。

Q&A

解決済

2回答

2426閲覧

Swift - JSONデータ取得方法 (ぐるなびAPI-応援口コミ)

ko-ru

総合スコア27

JSON

JSON(JavaScript Object Notation)は軽量なデータ記述言語の1つである。構文はJavaScriptをベースとしていますが、JavaScriptに限定されたものではなく、様々なソフトウェアやプログラミング言語間におけるデータの受け渡しが行えるように設計されています。

Swift

Swiftは、アップルのiOSおよびOS Xのためのプログラミング言語で、Objective-CやObjective-C++と共存することが意図されています

API

APIはApplication Programming Interfaceの略です。APIはプログラムにリクエストされるサービスがどのように動作するかを、デベロッパーが定めたものです。

0グッド

1クリップ

投稿2020/07/31 10:34

編集2020/08/01 05:49

前提・実現したいこと

現在SwiftでぐるなびAPIの応援口コミのデータを取得しようとしています。

該当ソースコード
・JsonStruct
・JSONデータ

発生している問題・エラーメッセージ

Jsonデータの取得はできておりますが、JSONStructの『case review = "0"』 の部分の数字(文字列)を動的に変更する方法がわからない状況です。 現在の挙動としては、『case review = "0"』としているため、データ件数が複数件あったとしても Jsonデータの1つ目しか取得できないです。

該当のソースコード

JSONStruct

1class JsonReviewsList { 2 // MARK: - Reviews 3 struct Reviews: Codable { 4 let response: Response 5 } 6 7 // MARK: - Response 8 struct Response: Codable { 9 let review: Review 10 let attributes: Attributes 11 let totalHitCount, hitPerPage: Int 12 13 enum CodingKeys: String, CodingKey { 14 15 case review = "0"  ※※※※※※※※※※※※※※※※※※※※動的に変化させたい(For文?) 16 case attributes = "@attributes" 17 case totalHitCount = "total_hit_count" 18 case hitPerPage = "hit_per_page" 19 } 20 } 21 22 // MARK: - Attributes 23 struct Attributes: Codable { 24 let apiVersion: String 25 26 enum CodingKeys: String, CodingKey { 27 case apiVersion = "api_version" 28 } 29 } 30 31 // MARK: - Review 32 struct Review: Codable { 33 let photo: Photo 34 } 35 36 // MARK: - Photo 37 struct Photo: Codable { 38 let voteID, photoGenreID, photoGenreName, photoSceneID: String 39 let photoSceneName, nickname, shopID, shopName: String 40 let shopURL: String 41 let prefname: String 42 let menuID: Int 43 let menuName: String 44 let menuFinishFlag: Int 45 let areanameL, areanameM, areanameS: String 46 let imageURL: ImageURL 47 let comment, totalScore, category, latitude: String 48 let longitude: String 49 let umasoCount: Int 50 let updateDate: Date 51 let messages: Messages 52 53 enum CodingKeys: String, CodingKey { 54 case voteID = "vote_id" 55 case photoGenreID = "photo_genre_id" 56 case photoGenreName = "photo_genre_name" 57 case photoSceneID = "photo_scene_id" 58 case photoSceneName = "photo_scene_name" 59 case nickname 60 case shopID = "shop_id" 61 case shopName = "shop_name" 62 case shopURL = "shop_url" 63 case prefname 64 case menuID = "menu_id" 65 case menuName = "menu_name" 66 case menuFinishFlag = "menu_finish_flag" 67 case areanameL = "areaname_l" 68 case areanameM = "areaname_m" 69 case areanameS = "areaname_s" 70 case imageURL = "image_url" 71 case comment 72 case totalScore = "total_score" 73 case category, latitude, longitude 74 case umasoCount = "umaso_count" 75 case updateDate = "update_date" 76 case messages 77 } 78 } 79 80 // MARK: - ImageURL 81 struct ImageURL: Codable { 82 let url1024, url320, url250, url200: String 83 84 enum CodingKeys: String, CodingKey { 85 case url1024 = "url_1024" 86 case url320 = "url_320" 87 case url250 = "url_250" 88 case url200 = "url_200" 89 } 90 } 91 92 // MARK: - Messages 93 struct Messages: Codable { 94 let userMessageCount, shopMessageCount: Int 95 let firstShopMessage: FirstShopMessage 96 97 enum CodingKeys: String, CodingKey { 98 case userMessageCount = "user_message_count" 99 case shopMessageCount = "shop_message_count" 100 case firstShopMessage = "first_shop_message" 101 } 102 } 103 104 // MARK: - FirstShopMessage 105 struct FirstShopMessage: Codable { 106 let messageBody, sendDate: String 107 108 enum CodingKeys: String, CodingKey { 109 case messageBody = "message_body" 110 case sendDate = "send_date" 111 } 112 } 113 114 115}
【JSONデータ】 { "response": { "@attributes": { "api_version": "v3" }, "total_hit_count": 1389, "hit_per_page": 15, "0": {   ←※※※※※※※※※※※※※※※※※※※※※※※※※※※"0"の数字が変化します。  "photo": { "vote_id": "95644", "photo_genre_id": "1", "photo_genre_name": "料理・ドリンク", "photo_scene_id": "", "photo_scene_name": "", "nickname": "ぐるなび会員", "shop_id": "6072772", "shop_name": "焼野菜 五十家", "shop_url": "https://r.gnavi.co.jp/3fjar9m50000/?ak=Y%2FdqTpL57s7y4VkTH%2BYkvUruWMirqD7LUAakY4OB0xs%3D", "prefname": "PREF26:京都府", "menu_id": 90968, "menu_name": "農園茄子と京お揚げの揚げ出し", "menu_finish_flag": 0, "areaname_l": "河原町・木屋町・先斗町", "areaname_m": "四条河原町周辺・寺町", "areaname_s": "四条河原町周辺・寺町", "image_url": { "url_1024": "https://mr.gnavi.co.jp/cont/menu_image/6f/25/95644_l.jpeg", "url_320": "https://mr.gnavi.co.jp/cont/menu_image/6f/25/95644.jpeg", "url_250": "https://mr.gnavi.co.jp/cont/menu_image/6f/25/95644_m.jpeg", "url_200": "https://mr.gnavi.co.jp/cont/menu_image/6f/25/95644_s.jpeg" }, "comment": "アツアツで美味しいです。", "total_score": "3.0", "category": "野菜料理", "distance": 0, "latitude": "", "longitude": "", "umaso_count": 0, "update_date": "2010-12-31T10:13:00+09:00", "messages": { "user_message_count": 0, "shop_message_count": 0, "first_shop_message": { "message_body": "", "send_date": "" } } } }, "1": {   ←※※※※※※※※※※※※※※※※※※※※※※※※※※※  "photo": { "vote_id": "", "photo_genre_id": "", ----------- 省略 -----------
参照元クラス let url = URL(string: ”ぐるなびAPI URL”) let request = URLRequest(url: url!) let session = URLSession.shared session.dataTask(with: request){(data, response, error)in if error == nil,  let data = data, ((response as? HTTPURLResponse) != nil) { //デコード let decoder: JSONDecoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 do { //データ取得成功 let json:JsonReviewsList.Reviews = try decoder.decode(JsonReviewsList.Reviews.self, from: data) let totalHitCount:Int = Int(json.total_hit_count) let jsonreview = json.reviews      for i in (0...totalHitCount-1) { print(jsonreview[i].shopID) print(jsonreview[i].shopName) } }catch {print ("error")} } }.resume()

ご教授よろしくお願いいたします。

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答2

0

ぐるなびAPIについては調査しきれないので、ここでは数字がキーのJSONが返ってきたときのデシリアライズ方法について紹介します。

質問でいただいたJSONをかなり簡略化して、下記のようなサンプルJSONを作成しました。

json

1{ 2 "response": { 3 "total_hit_count": 2, 4 "0": { 5 "shop_id": "6072772", 6 "shop_name": "店舗A" 7 }, 8 "1": { 9 "shop_id": "6072773", 10 "shop_name": "店舗B" 11 } 12 } 13}

CodingKeyを継承したキーリストは固定値(enum)である必要はないので、下記のように定義することができます。

swift

1struct CodingKeys: CodingKey { 2 var stringValue: String 3 init?(stringValue: String) { 4 self.stringValue = stringValue 5 } 6 7 var intValue: Int? 8 init?(intValue: Int) { 9 return nil 10 } 11 12 // 固定で返ってくるキー 13 static let total_hit_count = CodingKeys(stringValue: "total_hit_count")! 14}

コードの全体としては下記の通りです。ぐるなびAPIの数字部分がどこまで伸びるか仕様を把握できていないので、仮に 0 〜 total_hit_count までの数字のキーがあると想定しています。

swift

1import Foundation 2 3struct Reviews: Decodable { 4 let response: RakutenResponse 5} 6 7struct RakutenResponse: Decodable { 8 let total_hit_count: Int 9 let reviews: [Review] 10 11 // MARK: - codable 12 13 struct CodingKeys: CodingKey { 14 var stringValue: String 15 init?(stringValue: String) { 16 self.stringValue = stringValue 17 } 18 19 var intValue: Int? 20 init?(intValue: Int) { 21 return nil 22 } 23 24 // 固定で返ってくるキー 25 static let total_hit_count = CodingKeys(stringValue: "total_hit_count")! 26 } 27 28 init(from decoder: Decoder) throws { 29 let values = try decoder.container(keyedBy: CodingKeys.self) 30 total_hit_count = try values.decode(Int.self, forKey: .total_hit_count) 31 32 // NOTE: 数字部分の仕様がわからないので 0 〜 hit_count までデータを取得する 33 reviews = try (0 ..< total_hit_count) 34 .map { (index) -> Review? in 35 try values.decodeIfPresent(Review.self, forKey: CodingKeys(stringValue: "(index)")!) 36 } 37 .compactMap { $0 } 38 } 39} 40 41struct Review: Decodable { 42 let shop_id: String 43 let shop_name: String 44}

投稿2020/08/01 03:59

ch3cooh

総合スコア287

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

takasek

2020/08/01 05:05

たしかにCodingKeyをふたつ作らなくても、すべて同じCodingKeysにまとめてしまうほうがわかりやすいですね!
ch3cooh

2020/08/01 05:30

CodingKeyをふたつ作る発想はなかったので、関心しながらコード読ませていただきました! 文字列を書きたくなくていつもは↓のようにJSONのキー名に合わせて定義しているので、バラバラにCodingKeyを定義した方が明確に分けることができて、良いアイディアだなと。 ``` enum 固定用CodingKeys: String, CodingKey { case total_hit_count case hit_per_page } struct 可変用CodingKeys: CodingKey { //... } ```
ko-ru

2020/08/01 06:00

ご回答ありがとうございます。 takasekさんへの返信にもあるように、他クラスからJsonReviewsListを参照してデータを取得するような作りにしており、自環境に合わせて作成を試みましたが、うまく取得できない状況です。 知識不足で申し訳ございませんが、同じクラス内ではなく他クラスからJsonReviewsListを参照して表示する場合、教えていただいたコードのどの部分を変更することによって対応することができるのか教えて頂ければ幸いです。 コードを追加しましたのでご教授お願いいたします。
ko-ru

2020/08/01 09:13

解決できました! 回答いただいた2人の方々のコードを見比べながら問題解決することができました。 JSONデータの扱いに苦戦してはいますが、早めに慣れたい思います。 ありがとうございました。
guest

0

ベストアンサー

これはなかなかめんどくさいAPI設計ですね。もし社内でこのようなAPIがリリースされそうになったら、自分なら絶対世に出る前に修正します。Arrayとして表現されるべきでしょう。
とはいえ、こうなっちゃってるもんはしかたないですね。楽しそうなので挑戦してみました。
こんな感じでどうでしょうか。

CodingKeyは

  • ひとつのstructにひとつだけでなくてもよい
  • enumでなくてもよい

というのがポイントです。

swift

1let json = """ 2... 3※ https://api.gnavi.co.jp/api/tools/ からレスポンス引っ張ってきてコピペ 4... 5""".data(using: .utf8)! 6 7struct Root: Decodable { 8 struct Response: Decodable { 9 struct Review: Decodable { 10 struct Photo: Decodable { 11 enum CodingKeys: String, CodingKey { case voteID = "vote_id" } 12 let voteID: String 13 // 他のパラメータは読みやすさのため省略します 14 } 15 let photo: Photo 16 } 17 // attributes は回答には非本質的かつ冗長なので省略します。 18 // そもそもクライアントでattributes使う?というのも疑問で、要らないなら無視してもよいと思います 19 let totalHitCount, hitPerPage: Int 20 let reviews: [Review] 21 22 enum CodingKeys: String, CodingKey { 23 case totalHitCount = "total_hit_count" 24 case hitPerPage = "hit_per_page" 25 } 26 struct IndexCodingKey: CodingKey { 27 let stringValue: String 28 let intValue: Int? = nil 29 init(stringValue: String) { 30 self.stringValue = stringValue 31 } 32 init(intValue: Int) { 33 stringValue = String(intValue) 34 } 35 } 36 37 init(from decoder: Decoder) throws { 38 let container = try decoder.container(keyedBy: CodingKeys.self) 39 totalHitCount = try container.decode(Int.self, forKey: .totalHitCount) 40 hitPerPage = try container.decode(Int.self, forKey: .hitPerPage) 41 42 // ここからがミソ。forで回して0からインクリメントしてkeyを得て、 43 // そのkeyがデコードできるようならReviewをデコードして配列にappendしていく 44 // そのkeyがデコードできなくなったらbreakでループ脱出 45 let reviewsContainer = try decoder.container(keyedBy: IndexCodingKey.self) 46 var reviews: [Review] = [] 47 for i in 0... { 48 let key = IndexCodingKey(intValue: i) 49 guard reviewsContainer.contains(key) else { break } 50 reviews.append(try reviewsContainer.decode(Review.self, forKey: key)) 51 } 52 self.reviews = reviews 53 } 54 } 55 let response: Response 56} 57 58let root = try JSONDecoder().decode(Root.self, from: json) 59dump(root)

余談ですが、

  • JsonReviewsListがclassになってたのはなぜでしょう? データ構造は可能な限りstructを使った方が良いと思います
  • Encodeするつもりがないなら、protocolはCodableよりDecodableのほうが意図が絞られて良いです
  • url1024, url320, url250, url200はString型よりURL型のほうが意図を取りやすく不正な文字列の可能性を減らせて良いです
  • 各種IDが入り乱れていますが、取り違えを防ぐためにはそれぞれ独自のstructとして表現したほうがいいことが多いです(別にめんどくさい場合はコスパを鑑みてStringのままでもいい場合もあります)。参考記事: https://qiita.com/Mt-Hodaka/items/5e11dcbfb04040fa685f

投稿2020/08/01 02:58

編集2020/08/01 03:22
takasek

総合スコア34

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

ko-ru

2020/08/01 05:50

ご回答ありがとうございます。 CodingKeyのポイントに関して知識がなかったため勉強になります。 教えていただいた方法で取得できるのを確認できましたが、他のクラスからJsonReviewsListを参照する作りにしています。 なんとか、自分の環境に整形して試しては見ましたが、上手くいかないようです。。 コード更新しましたのでご教授お願いいたします。
takasek

2020/08/01 06:07

元質問のコードが変更されてしまうと、どこにどういう回答があったのかわからなくなってしまうので、どこがうまくいかないかを一問一答の形で追えるほうがよいかと思います。お困りなのはここですね? ```swift session.dataTask(with: request){(data, response, error)in if error == nil,  let data = data, ((response as? HTTPURLResponse) != nil) { //デコード let decoder: JSONDecoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 do { //データ取得成功 let json:JsonReviewsList.Reviews = try decoder.decode(JsonReviewsList.Reviews.self, from: data) let totalHitCount:Int = Int(json.total_hit_count) let jsonreview = json.reviews      for i in (0...totalHitCount-1) { print(jsonreview[i].shopID) print(jsonreview[i].shopName) } }catch {print ("error")} } }.resume() ``` レスポンスのJSONはルートが `{ "response": { ... } }` なので、当然 `JsonReviewsList.Reviews` とは構造が合いません。 また、forを回すのは `init(from decoder: Decoder) throws` 内に書いてください。 回答したコードは(私のものもch3coohさんのものも)動かせるコードになっているはずなので、どういう意図のコードなのかを読み解きながらXcodeのエラーメッセージを見つつ修正すればうまくいくと思いますので、頑張ってみてください。
ko-ru

2020/08/01 09:10

解決できました!! ありがとうございました。 voteID以外も定義すると、なぜか通らず、menuIDの場所までなら通る状況で、 悩んでいましたが、ViewDidLoad()のとこにDispatchQueue.main.async を書いていてその中でfunctionの読み込みを行なっていた為、うまく動作しなかったようです。 DispatchQueue.main.async の外に出し、functionを読み込むようにするとデータを全て 取得することができました。 DispatchQueueをなんとなく使用していますが、その辺りの知識も勉強しようと思います。 ありがとうございました。
takasek

2020/08/01 09:54

うーん…DispatchQueueと問題の繋がりがちょっとぴんときませんね… 色々な問題が組み合わさって、切り分けづらくなっているように感じます。 プログラミングは問題を分割して各個撃破していくのが基本です。手慣れた人でも、いきなり大きな物を作ろうとするとうまくいかないものです。 今回の問題であれば、まずch3coohさんのコードのような、ぐるなびAPIそのものよりも単純なサンプルを作るのはとても良いアプローチです。 なるべくそういう小さなコードで動作確認して、うまく動くことが確認できたもの同士を組み合わせるように意識してみてください。一見遠回りでも、トータルのスピードも上がり、安定した開発ができるようになります。
ko-ru

2020/08/01 13:04

DispatchQueueの中に複数個処理を記載していた為、 競合してしまったのかなと推測していますが、もっと切り分けが必要そうです。 小さなコードで動作確認しながら進めていく方が問題重ならず切り分けにかかる時間を 短縮できるかと思うので、これからそのように進めていきたいと思います。 ここからは、問の内容は解決している為、無視していただいても構いません。 DispatchQueueの中を本件の処理のみにすると正常に動作しました。 ちなみにうまく動作しないときにdecoder.decode(reviewList.Root.self, from: data) を見てみると下記エラー表示になっていました。 ``` ▿ DecodingError ▿ keyNotFound : 2 elements - .0 : CodingKeys(stringValue: "menu_id", intValue: nil) ▿ .1 : Context ▿ codingPath : 3 elements - 0 : CodingKeys(stringValue: "response", intValue: nil) ▿ 1 : IndexCodingKey(stringValue: "1", intValue: nil) - stringValue : "1" - intValue : nil - 2 : CodingKeys(stringValue: "photo", intValue: nil) - debugDescription : "No value associated with key CodingKeys(stringValue: \"menu_id\", intValue: nil) (\"menu_id\")." - underlyingError : nil ```
ko-ru

2020/08/02 10:43 編集

発生していたエラーを解決する方法を調べたので一応共有しておきます。 取得するJSON形式を確認した所"menu_id"が含まれないことがあるようです。 ※menu_id以外も幾つか、、 その為、JSONの中身によってエラーが出たり出なかったりとしていた様です。 今回の場合、Optional型にすることで回避できるようです。 Optional型に変更することで解決できました。 Dispatchは原因ではなかったようです;; 参照:https://www.appsdissected.com/json-codable-decodingerror-quicktype/
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.35%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問