Swift 3でJSONを正しく解析する


123

JSON応答をフェッチして、結果を変数に格納しようとしています。Xcode 8のGMバージョンがリリースされるまで、Swiftの以前のリリースでこのコードのバージョンを機能させてきました。StackOverflow:Swift 2 Parsing JSON- Swift 3 でタイプ 'AnyObject'の値JSON Parsingに添字を付けることはできません

しかし、そこに伝えられたアイデアはこのシナリオには当てはまらないようです。

Swift 3でJSON応答を正しく解析するにはどうすればよいですか?Swift 3でのJSONの読み取り方法に変更はありますか?

以下は問題のコードです(プレイグラウンドで実行できます):

import Cocoa

let url = "https://api.forecast.io/forecast/apiKey/37.5673776,122.048951"

if let url = NSURL(string: url) {
    if let data = try? Data(contentsOf: url as URL) {
        do {
            let parsedData = try JSONSerialization.jsonObject(with: data as Data, options: .allowFragments)

        //Store response in NSDictionary for easy access
        let dict = parsedData as? NSDictionary

        let currentConditions = "\(dict!["currently"]!)"

        //This produces an error, Type 'Any' has no subscript members
        let currentTemperatureF = ("\(dict!["currently"]!["temperature"]!!)" as NSString).doubleValue

            //Display all current conditions from API
            print(currentConditions)

            //Output the current temperature in Fahrenheit
            print(currentTemperatureF)

        }
        //else throw an error detailing what went wrong
        catch let error as NSError {
            print("Details of JSON parsing error:\n \(error)")
        }
    }
}

編集:これは後のAPI呼び出しの結果のサンプルですprint(currentConditions)

["icon": partly-cloudy-night, "precipProbability": 0, "pressure": 1015.39, "humidity": 0.75, "precipIntensity": 0, "windSpeed": 6.04, "summary": Partly Cloudy, "ozone": 321.13, "temperature": 49.45, "dewPoint": 41.75, "apparentTemperature": 47, "windBearing": 332, "cloudCover": 0.28, "time": 1480846460]

API呼び出しから返されたサンプルデータを配置できますか?
ユーザーの

1
ええ、print(currentConditions)の後に印刷された結果のサンプルを追加しました。それが役に立てば幸い。
user2563039 16

回答:


172

まず、リモートURLからデータを同期的にロードないでくださいURLSession。常に、非同期メソッドを使用してください。

「Any」には添え字メンバーがありません

これは、コンパイラーが中間オブジェクトのタイプを認識していないため(たとえばcurrently["currently"]!["temperature"]NSDictionary、コンパイラーのようにFoundationコレクションタイプを使用しているため、タイプについてまったく認識していません。

さらに、Swift 3では、すべての添え字付きオブジェクトのタイプについてコンパイラーに通知する必要があります。

JSONシリアル化の結果を実際の型にキャストする必要があります。

このコードはURLSession、Swiftネイティブ型のみを使用します

let urlString = "https://api.forecast.io/forecast/apiKey/37.5673776,122.048951"

let url = URL(string: urlString)
URLSession.shared.dataTask(with:url!) { (data, response, error) in
  if error != nil {
    print(error)
  } else {
    do {

      let parsedData = try JSONSerialization.jsonObject(with: data!) as! [String:Any]
      let currentConditions = parsedData["currently"] as! [String:Any]

      print(currentConditions)

      let currentTemperatureF = currentConditions["temperature"] as! Double
      print(currentTemperatureF)
    } catch let error as NSError {
      print(error)
    }
  }

}.resume()

currentConditionsあなたが書くことができるすべてのキー/値のペアを印刷するには

 let currentConditions = parsedData["currently"] as! [String:Any]

  for (key, value) in currentConditions {
    print("\(key) - \(value) ")
  }

に関するメモjsonObject(with data

多くの(すべてのように思われる)チュートリアルが提案する.mutableContainers.mutableLeaves、Swiftではまったくナンセンスなオプションです。2つのオプションは、結果をNSMutable...オブジェクトに割り当てるための従来のObjective-Cオプションです。Swiftでは、すべてのvariableはデフォルトで変更可能であり、これらのオプションのいずれかを渡して結果をlet定数に割り当ててもまったく効果がありません。さらに、ほとんどの実装では、逆シリアル化されたJSONを変更することはありません。

スイフトに有用であるのみ(まれな)オプションがある.allowFragmentsJSONルートオブジェクトは値型とすることができる場合ならば必要とされる(StringNumberBoolまたはnull)ではなくコレクション・タイプのいずれか(arrayまたはdictionary)。ただし、通常は、オプションなしoptionsを意味するパラメーターを省略します

================================================== =========================

JSONを解析するための一般的な考慮事項

JSONは適切に配置されたテキスト形式です。JSON文字列を読み取るのは非常に簡単です。文字列を注意深く読んでください。型は6つしかありません。2つのコレクション型と4つの値型です。


コレクションの種類は

  • 配列 -JSON:大括弧内のオブジェクト[]-Swift:[Any]ほとんどの場合[[String:Any]]
  • 辞書 -JSON:中括弧内のオブジェクト{}-Swift:[String:Any]

値のタイプは

  • 文字列 -JSON:二重引用符"Foo"で囲まれた任意の値、"123"または"false"- またはSwift:String
  • 番号 - JSON:数値ではない二重引用符で123123.0-スウィフト:IntまたはDouble
  • ブール - JSON:truefalse いない二重引用符で-スウィフト:trueまたはfalse
  • null -JSON:null– Swift:NSNull

JSON仕様によれば、辞書のすべてのキーはである必要がありますString


基本的には、オプションのバインディングを使用してオプションを安全にアンラップすることを常にお勧めします

ルートオブジェクトが辞書({})の場合、型を[String:Any]

if let parsedData = try JSONSerialization.jsonObject(with: data!) as? [String:Any] { ...

キーで値を取得します(OneOfSupportedJSONTypes上記のように、JSONコレクションまたは値のタイプです)。

if let foo = parsedData["foo"] as? OneOfSupportedJSONTypes {
    print(foo)
} 

ルートオブジェクトが配列([])の場合、型を[[String:Any]]

if let parsedData = try JSONSerialization.jsonObject(with: data!) as? [[String:Any]] { ...

と配列を繰り返します

for item in parsedData {
    print(item)
}

特定のインデックスのアイテムが必要な場合は、インデックスが存在するかどうかも確認してください

if let parsedData = try JSONSerialization.jsonObject(with: data!) as? [[String:Any]], parsedData.count > 2,
   let item = parsedData[2] as? OneOfSupportedJSONTypes {
      print(item)
    }
}

JSONがコレクションタイプではなく、単に値タイプの1つであるまれなケースでは、.allowFragmentsオプションを渡して、結果を適切な値タイプにキャストする必要があります。

if let parsedData = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? String { ...

AppleはSwiftブログで包括的な記事を公開しています:Swift でのJSONの操作


================================================== =========================

Swift 4以降では、CodableプロトコルはJSONを直接構造体/クラスに解析するより便利な方法を提供します。

たとえば、質問で指定されたJSONサンプル(わずかに変更)

let jsonString = """
{"icon": "partly-cloudy-night", "precipProbability": 0, "pressure": 1015.39, "humidity": 0.75, "precip_intensity": 0, "wind_speed": 6.04, "summary": "Partly Cloudy", "ozone": 321.13, "temperature": 49.45, "dew_point": 41.75, "apparent_temperature": 47, "wind_bearing": 332, "cloud_cover": 0.28, "time": 1480846460}
"""

構造体にデコードできますWeather。Swiftタイプは上記と同じです。追加のオプションがいくつかあります。

  • を表す文字列は、URLとして直接デコードできますURL
  • time整数として復号することができるDatedateDecodingStrategy .secondsSince1970
  • snaked_cased JSONキーをキャメルケースに変換するには、keyDecodingStrategy .convertFromSnakeCase

struct Weather: Decodable {
    let icon, summary: String
    let pressure: Double, humidity, windSpeed : Double
    let ozone, temperature, dewPoint, cloudCover: Double
    let precipProbability, precipIntensity, apparentTemperature, windBearing : Int
    let time: Date
}

let data = Data(jsonString.utf8)
do {
    let decoder = JSONDecoder()
    decoder.dateDecodingStrategy = .secondsSince1970
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    let result = try decoder.decode(Weather.self, from: data)
    print(result)
} catch {
    print(error)
}

その他のコード化可能なソース:


これは非常に役立ちます。上記のコードがプレイグラウンドで実行されたときに、目に見える出力が表示されないのはなぜですか。
user2563039

上記のように、あいまいな結果をdict!["currently"]!辞書にキャストする必要があります。これにより、コンパイラーは後続の鍵サブスクリプションを安全に推測できます。
バディアン

1
Appleの記事はかなりかっこいいですが、なぜかswift 3で実行することができませんでした。Type [String:Any]について文句を言う?下付きのメンバーはありません。他にもいくつか問題がありましたが、回避することができました。誰かが実際に実行するコードの例を持っていますか?
シェード2016年

@Shades質問してコードを投稿してください。ほとんどの場合、問題はラップされていないオプションに関連しています。
vadian

API呼び出しから返されたサンプルデータを配置できますか?
ユーザーの

12

Xcode 8 Beta 6 for Swift 3で起こった大きな変更は、IDがでAnyはなくとしてインポートされるようになったことですAnyObject

これは、parsedDataが最も可能性の高い型の辞書として返されることを意味し[Any:Any]ます。デバッガーを使用しないと、キャストの動作を正確に伝えることはできませんでしたNSDictionaryが、表示されているエラーはdict!["currently"]!はタイプがAny

それで、これをどのように解決しますか?あなたがそれを参照した方法から、私は仮定しますdict!["currently"]!は辞書であると思います、そしてあなたには多くのオプションがあります:

まず、次のようなことができます。

let currentConditionsDictionary: [String: AnyObject] = dict!["currently"]! as! [String: AnyObject]  

これにより、値を照会できる辞書オブジェクトが得られるため、次のように温度を取得できます。

let currentTemperatureF = currentConditionsDictionary["temperature"] as! Double

または、必要に応じてインラインで実行できます。

let currentTemperatureF = (dict!["currently"]! as! [String: AnyObject])["temperature"]! as! Double

これが役に立てば幸いです。テストするためのサンプルアプリを書く時間がなかったと思います。

最後の注意点:最も簡単なことは、JSONペイロードを[String: AnyObject]最初からキャストすることです。

let parsedData = try JSONSerialization.jsonObject(with: data as Data, options: .allowFragments) as! Dictionary<String, AnyObject>

dict!["currently"]! as! [String: String]クラッシュします
バディアン

クラッシュしたのが【「温度」】でした!!また、StringをNSStringにキャストしようとしています-新しいソリューションが動作することを確認しましたswiftlang.ng.bluemix.net/#/repl/57d3bc683a422409bf36c391
discorevilo

[String: String]数値が2つあるため、まったく機能しません。Macのプレイグラウンドでは機能しません
vadian

@vadianああ、はい、私はそれらに気づかなかったし、迅速なサンドボックスはそれらを便利に隠した!-[もう一度]を修正(macOSでテスト)。これを指摘していただきありがとうございます:)
discorevilo

6
let str = "{\"names\": [\"Bob\", \"Tim\", \"Tina\"]}"

let data = str.data(using: String.Encoding.utf8, allowLossyConversion: false)!

do {
    let json = try JSONSerialization.jsonObject(with: data, options: []) as! [String: AnyObject]
    if let names = json["names"] as? [String] 
{
        print(names)
}
} catch let error as NSError {
    print("Failed to load: \(error.localizedDescription)")
}

5

私はまさにこの目的のためにquicktypeを構築しました。サンプルJSONを貼り付けるだけで、quicktypeがAPIデータのこのタイプ階層を生成します。

struct Forecast {
    let hourly: Hourly
    let daily: Daily
    let currently: Currently
    let flags: Flags
    let longitude: Double
    let latitude: Double
    let offset: Int
    let timezone: String
}

struct Hourly {
    let icon: String
    let data: [Currently]
    let summary: String
}

struct Daily {
    let icon: String
    let data: [Datum]
    let summary: String
}

struct Datum {
    let precipIntensityMax: Double
    let apparentTemperatureMinTime: Int
    let apparentTemperatureLowTime: Int
    let apparentTemperatureHighTime: Int
    let apparentTemperatureHigh: Double
    let apparentTemperatureLow: Double
    let apparentTemperatureMaxTime: Int
    let apparentTemperatureMax: Double
    let apparentTemperatureMin: Double
    let icon: String
    let dewPoint: Double
    let cloudCover: Double
    let humidity: Double
    let ozone: Double
    let moonPhase: Double
    let precipIntensity: Double
    let temperatureHigh: Double
    let pressure: Double
    let precipProbability: Double
    let precipIntensityMaxTime: Int
    let precipType: String?
    let sunriseTime: Int
    let summary: String
    let sunsetTime: Int
    let temperatureMax: Double
    let time: Int
    let temperatureLow: Double
    let temperatureHighTime: Int
    let temperatureLowTime: Int
    let temperatureMin: Double
    let temperatureMaxTime: Int
    let temperatureMinTime: Int
    let uvIndexTime: Int
    let windGust: Double
    let uvIndex: Int
    let windBearing: Int
    let windGustTime: Int
    let windSpeed: Double
}

struct Currently {
    let precipProbability: Double
    let humidity: Double
    let cloudCover: Double
    let apparentTemperature: Double
    let dewPoint: Double
    let ozone: Double
    let icon: String
    let precipIntensity: Double
    let temperature: Double
    let pressure: Double
    let precipType: String?
    let summary: String
    let uvIndex: Int
    let windGust: Double
    let time: Int
    let windBearing: Int
    let windSpeed: Double
}

struct Flags {
    let sources: [String]
    let isdStations: [String]
    let units: String
}

また、依存関係のないマーシャリングコードを生成して、JSON文字列を取得する便利なコンストラクターを含むに戻り値をJSONSerialization.jsonObject組み込みForecast、強く型付けされたForecast値をすばやく解析してそのフィールドにアクセスできるようにします。

let forecast = Forecast.from(json: jsonString)!
print(forecast.daily.data[0].windGustTime)

npmからquicktypeをインストールするnpm i -g quicktype、Web UI使用して完全な生成コードを取得し、プレイグラウンドに貼り付けることができます。


リンクが失敗しています。
描きました。

素晴らしい時間節約!よくやった!
marko 2018

1
素晴らしいツール。ありがとう。
Ahmet

4

isConnectToNetwork-Functionこの投稿のおかげで、その後更新されました。

私はそれに追加のメソッドを書きました:

import SystemConfiguration

func loadingJSON(_ link:String, postString:String, completionHandler: @escaping (_ JSONObject: AnyObject) -> ()) {

    if(isConnectedToNetwork() == false){
        completionHandler("-1" as AnyObject)
        return
    }

    let request = NSMutableURLRequest(url: URL(string: link)!)
    request.httpMethod = "POST"
    request.httpBody = postString.data(using: String.Encoding.utf8)

    let task = URLSession.shared.dataTask(with: request as URLRequest) { data, response, error in
        guard error == nil && data != nil else { // check for fundamental networking error
            print("error=\(error)")
            return
        }

        if let httpStatus = response as? HTTPURLResponse , httpStatus.statusCode != 200 { // check for http errors
            print("statusCode should be 200, but is \(httpStatus.statusCode)")
            print("response = \(response)")
        }
        //JSON successfull
        do {
            let parseJSON = try JSONSerialization.jsonObject(with: data!, options: .allowFragments)
            DispatchQueue.main.async(execute: {
                completionHandler(parseJSON as AnyObject)
            });
        } catch let error as NSError {
            print("Failed to load: \(error.localizedDescription)")
        }
    }
    task.resume()
}

func isConnectedToNetwork() -> Bool {

    var zeroAddress = sockaddr_in(sin_len: 0, sin_family: 0, sin_port: 0, sin_addr: in_addr(s_addr: 0), sin_zero: (0, 0, 0, 0, 0, 0, 0, 0))
    zeroAddress.sin_len = UInt8(MemoryLayout.size(ofValue: zeroAddress))
    zeroAddress.sin_family = sa_family_t(AF_INET)

    let defaultRouteReachability = withUnsafePointer(to: &zeroAddress) {
        $0.withMemoryRebound(to: sockaddr.self, capacity: 1) {zeroSockAddress in
            SCNetworkReachabilityCreateWithAddress(nil, zeroSockAddress)
        }
    }

    var flags: SCNetworkReachabilityFlags = SCNetworkReachabilityFlags(rawValue: 0)
    if SCNetworkReachabilityGetFlags(defaultRouteReachability!, &flags) == false {
        return false
    }

    let isReachable = (flags.rawValue & UInt32(kSCNetworkFlagsReachable)) != 0
    let needsConnection = (flags.rawValue & UInt32(kSCNetworkFlagsConnectionRequired)) != 0
    let ret = (isReachable && !needsConnection)

    return ret
}

これで、アプリのどこからでも簡単に呼び出すことができます

loadingJSON("yourDomain.com/login.php", postString:"email=\(userEmail!)&password=\(password!)") { parseJSON in

    if(String(describing: parseJSON) == "-1"){
        print("No Internet")
    } else {

    if let loginSuccessfull = parseJSON["loginSuccessfull"] as? Bool {
        //... do stuff
    }
}

マルコ、この機能をプロジェクトに追加する最良の方法は何でしょうか?コントローラーまたはモデルでは?
KamalPanhwar 2017年

ちょっと、最初のメソッドfunc loadingJSON(...)を追加の迅速なファイルまたはクラスに追加するだけです。その後、プロジェクト内のすべてのコントローラーからこれを呼び出すことができます
Marco Weber

私は試してみましたが、完全なソリューションを示すアイデアと、ヘルパーメソッドisConnectedToNetwork()を含めてそれを使用する方法も気に入っています。おそらく、それを良いコードで実装する方法のアイデアです
Amr Angry

そのため、私は新しいswiftファイル(プロジェクトツリーの左klick、新しいファイル...、swiftファイル)を使用して、jsonhelper.swiftという名前を付けました。このファイルには、最初のコード、loadingJSON()およびisConnectedToNetwork()を配置します。その後、この2つの関数をプロジェクトのすべての部分で使用できます。たとえば、loginVCでは、2番目のコードを使用できるログインボタンのアクションとして、ドメイン、投稿文字列、paseJsonの値(parseJSON ["loginSuccessfull"])を変更して、次のコードに一致させる必要があります。あなたのphpファイル
Marco Weber

0

Swiftには強力な型推論があります。「if let」または「guard let」のボイラープレートを取り除き、機能的なアプローチを使用してアンラップを強制します。

  1. ここに私たちのJSONがあります。オプションのJSONまたは通常を使用できます。私の例ではオプションを使用しています:

    let json: Dictionary<String, Any>? = ["current": ["temperature": 10]]
  1. ヘルパー関数。それらを1回だけ記述してから、任意の辞書で再利用する必要があります。

    /// Curry
    public func curry<A, B, C>(_ f: @escaping (A, B) -> C) -> (A) -> (B) -> C {
        return { a in
            { f(a, $0) }
        }
    }

    /// Function that takes key and optional dictionary and returns optional value
    public func extract<Key, Value>(_ key: Key, _ json: Dictionary<Key, Any>?) -> Value? {
        return json.flatMap {
            cast($0[key])
        }
    }

    /// Function that takes key and return function that takes optional dictionary and returns optional value
    public func extract<Key, Value>(_ key: Key) -> (Dictionary<Key, Any>?) -> Value? {
        return curry(extract)(key)
    }

    /// Precedence group for our operator
    precedencegroup RightApplyPrecedence {
        associativity: right
        higherThan: AssignmentPrecedence
        lowerThan: TernaryPrecedence
    }

    /// Apply. g § f § a === g(f(a))
    infix operator § : RightApplyPrecedence
    public func §<A, B>(_ f: (A) -> B, _ a: A) -> B {
        return f(a)
    }

    /// Wrapper around operator "as".
    public func cast<A, B>(_ a: A) -> B? {
        return a as? B
    }
  1. そしてここに私たちの魔法があります-値を抽出します:

    let temperature = (extract("temperature") § extract("current") § json) ?? NSNotFound

コードは1行だけで、強制的に展開したり手動で型キャストしたりする必要はありません。このコードはプレイグラウンドで機能するため、コピーして確認できます。これがGitHubでの実装です。


0

これは、問題を解決するもう1つの方法です。以下の解決策をチェックしてください。それがあなたを助けることを願っています。

let str = "{\"names\": [\"Bob\", \"Tim\", \"Tina\"]}"
let data = str.data(using: String.Encoding.utf8, allowLossyConversion: false)!
do {
    let json = try JSONSerialization.jsonObject(with: data, options: []) as! [String: AnyObject]
    if let names = json["names"] as? [String] {
        print(names)
    }
} catch let error as NSError {
    print("Failed to load: \(error.localizedDescription)")
}

0

問題は、API対話メソッドにあります。JSON解析は構文のみが変更されています。主な問題は、データのフェッチ方法にあります。使用しているのは、データを取得する同期方法です。これはすべての場合で機能するわけではありません。使用する必要があるのは、データをフェッチする非同期の方法です。この方法では、APIを介してデータを要求し、データで応答するのを待つ必要があります。これは、URLセッションやのようなサードパーティのライブラリで実現できますAlamofire。以下は、URLセッションメソッドのコードです。

let urlString = "https://api.forecast.io/forecast/apiKey/37.5673776,122.048951"
let url = URL.init(string: urlString)
URLSession.shared.dataTask(with:url!) { (data, response, error) in
    guard error == nil else {
        print(error)
    }
    do {
        let Data = try JSONSerialization.jsonObject(with: data!) as! [String:Any]
        // Note if your data is coming in Array you should be using [Any]()
        //Now your data is parsed in Data variable and you can use it normally
        let currentConditions = Data["currently"] as! [String:Any]
        print(currentConditions)
        let currentTemperatureF = currentConditions["temperature"] as! Double
        print(currentTemperatureF)
    } catch let error as NSError {
        print(error)
    }
}.resume()

0
{
    "User":[
      {
        "FirstUser":{
        "name":"John"
        },
       "Information":"XY",
        "SecondUser":{
        "name":"Tom"
      }
     }
   ]
}

以前のjsonを使用してモデルを作成する場合このリンクを使用して[ブログ]:http : //www.jsoncafe.comでCodable構造または任意の形式を生成します

型番

import Foundation
struct RootClass : Codable {
    let user : [Users]?
    enum CodingKeys: String, CodingKey {
        case user = "User"
    }

    init(from decoder: Decoder) throws {
        let values = try? decoder.container(keyedBy: CodingKeys.self)
        user = try? values?.decodeIfPresent([Users].self, forKey: .user)
    }
}

struct Users : Codable {
    let firstUser : FirstUser?
    let information : String?
    let secondUser : SecondUser?
    enum CodingKeys: String, CodingKey {
        case firstUser = "FirstUser"
        case information = "Information"
        case secondUser = "SecondUser"
    }
    init(from decoder: Decoder) throws {
        let values = try? decoder.container(keyedBy: CodingKeys.self)
        firstUser = try? FirstUser(from: decoder)
        information = try? values?.decodeIfPresent(String.self, forKey: .information)
        secondUser = try? SecondUser(from: decoder)
    }
}
struct SecondUser : Codable {
    let name : String?
    enum CodingKeys: String, CodingKey {
        case name = "name"
    }
    init(from decoder: Decoder) throws {
        let values = try? decoder.container(keyedBy: CodingKeys.self)
        name = try? values?.decodeIfPresent(String.self, forKey: .name)
    }
}
struct FirstUser : Codable {
    let name : String?
    enum CodingKeys: String, CodingKey {
        case name = "name"
    }
    init(from decoder: Decoder) throws {
        let values = try? decoder.container(keyedBy: CodingKeys.self)
        name = try? values?.decodeIfPresent(String.self, forKey: .name)
    }
}

解析

    do {
        let res = try JSONDecoder().decode(RootClass.self, from: data)
        print(res?.user?.first?.firstUser?.name ?? "Yours optional value")
    } catch {
        print(error)
    }
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.