【问题标题】:Correctly Parsing JSON in Swift 3在 Swift 3 中正确解析 JSON
【发布时间】:2019-03-27 04:42:33
【问题描述】:

我正在尝试获取 JSON 响应并将结果存储在变量中。在 Xcode 8 的 GM 版本发布之前,我已经在以前的 Swift 版本中使用了此代码的版本。我在 StackOverflow 上查看了一些类似的帖子:Swift 2 Parsing JSON - Cannot subscript a value of type 'AnyObject'JSON Parsing in Swift 3

但是,那里传达的想法似乎不适用于这种情况。

如何在 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)")
        }
    }
}

编辑:这是print(currentConditions)之后的API调用结果示例

["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 调用返回的样本数据放上去吗?
  • 是的,我刚刚添加了一个在 print(currentConditions) 之后打印的结果示例。希望对您有所帮助。
  • 使用 Codable 协议在 swift4 中解析 json stackoverflow.com/a/52931265/9316566

标签: json swift parsing swift3 xcode8


【解决方案1】:

斯威夫特 5 无法从您的 api 获取数据。 解析 json 的最简单方法是使用 Decodable 协议。或Codable (Encodable & Decodable)。 例如:

let json = """
{
    "dueDate": {
        "year": 2021,
        "month": 2,
        "day": 17
    }
}
"""

struct WrapperModel: Codable {
    var dueDate: DueDate
}

struct DueDate: Codable {
    var year: Int
    var month: Int
    var day: Int
}

let jsonData = Data(json.utf8)

let decoder = JSONDecoder()

do {
    let model = try decoder.decode(WrapperModel.self, from: jsonData)
    print(model)
} catch {
    print(error.localizedDescription)
}

【讨论】:

    【解决方案2】:

    Swift 具有强大的类型推断功能。让我们摆脱“if let”或“guard let”样板文件并使用函数式方法强制展开:

    1. 这是我们的 JSON。我们可以使用可选的 JSON 或通常的。在我们的示例中,我使用的是可选的:
    let json: Dictionary<String, Any>? = ["current": ["temperature": 10]]
    
    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
    

    只需一行代码,无需强制解包或手动类型转换。此代码在 Playground 中有效,因此您可以复制并检查它。这是一个实现on GitHub.

    【讨论】:

      【解决方案3】:
      {
          "User":[
            {
              "FirstUser":{
              "name":"John"
              },
             "Information":"XY",
              "SecondUser":{
              "name":"Tom"
            }
           }
         ]
      }
      

      如果我使用以前的 json 创建模型 使用此链接 [博客]:http://www.jsoncafe.com 生成可编码结构或任何格式

      型号

      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)
          }
      

      【讨论】:

        【解决方案4】:

        问题在于 API 交互方法。 JSON 解析仅在语法上有所改变。主要问题在于获取数据的方式。您使用的是同步获取数据的方式。这并不适用于所有情况。您应该使用的是一种异步方式来获取数据。这样,您必须通过 API 请求数据并等待它响应数据。您可以使用 URL 会话和第三方库(如 Alamofire)来实现此目的。下面是 URL Session 方法的代码。

        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()
        

        【讨论】:

          【解决方案5】:

          这是解决问题的另一种方法。因此,请查看以下解决方案。希望对你有帮助。

          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)")
          }
          

          【讨论】:

            【解决方案6】:

            更新了isConnectToNetwork-Function,感谢post

            我为它写了一个额外的方法:

            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
                }
            }
            

            【讨论】:

            • Marco,在项目中添加此功能的最佳方法是什么?在任何控制器或模型中?
            • 嘿,您只需将第一个方法 func loadingJSON(...) 添加到一个额外的 swift 文件或类中。之后,您可以从项目中的每个控制器调用它
            • 我确实尝试过,但我喜欢展示完整解决方案以及如何使用它的想法,包括辅助方法 isConnectedToNetwork(),我想知道如何用好的代码实现它
            • 所以我只使用了一个新的 swift 文件(在您的项目树上单击鼠标左键,新文件 ...,swift 文件)并将其命名为 jsonhelper.swift。在这个文件中放置第一个代码,loadingJSON() 和 isConnectedToNetwork()。之后,您可以在项目的每个部分中使用这两个功能。例如在 loginVC 中,作为登录按钮的操作,您可以使用第二个代码,显然您必须更改域、post 字符串和 paseJson 值 (parseJSON["loginSuccessfull"]) 以便它们匹配你的 php 文件
            【解决方案7】:

            首先永远不要从远程 URL 同步加载数据,始终使用异步方法,例如 URLSession

            'Any' 没有下标成员

            发生是因为编译器不知道中间对象是什么类型(例如["currently"]!["temperature"] 中的currently),并且由于您使用的是像NSDictionary 这样的Foundation 集合类型,编译器根本不知道该类型.

            此外,在 Swift 3 中,需要告知编译器 all 下标对象的类型。

            您必须将 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 中完全是无稽之谈。这两个选项是传统的 Objective-C 选项,用于将结果分配给 NSMutable... 对象。在 Swift 中,任何 variable 默认情况下都是可变的,并且传递任何这些选项并将结果分配给 let 常量根本没有任何效果。此外,大多数实现无论如何都不会改变反序列化的 JSON。

            在 Swift 中唯一有用的(罕见的)选项是 .allowFragments,如果 JSON 根对象可以是值类型(StringNumberBoolnull),则需要此选项比集合类型之一(arraydictionary)。但通常省略options 参数,这意味着没有选项

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

            解析 JSON 的一些一般注意事项

            JSON 是一种排列良好的文本格式。读取 JSON 字符串非常容易。 仔细阅读字符串。只有六种不同的类型——两种集合类型和四种值类型。


            集合类型是

            • 数组 - JSON:方括号中的对象[] - Swift:[Any] 但在大多数情况下[[String:Any]]
            • 字典 - JSON:花括号中的对象{} - Swift:[String:Any]

            值类型是

            • 字符串 - JSON:双引号中的任何值 "Foo",甚至是 "123""false" - Swift:String
            • 数字 - JSON:数字值not 用双引号 123123.0 – Swift:IntDouble
            • Bool - JSON:truefalse not 双引号 - Swift:truefalse
            • null - JSON:null - 斯威夫特: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 只是一种值类型而不是集合类型的极少数情况下,您必须传递 .allowFragments 选项并将结果转换为适当的值类型,例如

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

            Apple 在 Swift 博客上发表了一篇综合文章:Working with JSON in Swift


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

            在 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 整数可以用dateDecodingStrategy .secondsSince1970 解码为Date
            • snaked_cased JSON 键可以转换为 camelCasekeyDecodingStrategy .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)
            }
            

            其他可编码来源:

            【讨论】:

            • 这很有帮助。我只是好奇为什么上面的代码在操场上运行时没有显示可见的输出。
            • 如上所述,您需要将dict!["currently"]! 的模棱两可的结果强制转换为编译器可以安全推断后续密钥订阅的字典。
            • Apple 文章很酷,但由于某种原因,我无法使用 swift 3 运行它。它抱怨类型 [String : Any]?没有下标成员。还有其他一些问题,但我能够解决它。任何人都有实际运行的代码示例?
            • @Shades 提出问题并发布您的代码。您的问题很可能与未包装的选项有关。
            • 你能把你的 API 调用返回的样本数据放上去吗?
            【解决方案8】:

            我正是为此目的构建了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
            }
            

            它还生成无依赖的封送代码以将JSONSerialization.jsonObject 的返回值哄骗到Forecast,包括一个采用JSON 字符串的便捷构造函数,因此您可以快速解析强类型的Forecast 值并访问其字段:

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

            您可以使用 npm i -g quicktypeuse the web UI 从 npm 安装 quicktype,以获取完整的生成代码并粘贴到您的 Playground 中。

            【讨论】:

            • 链接失败。
            • 非常节省时间!干得好!
            • 出色的工具。谢谢。
            【解决方案9】:
            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)")
            }
            

            【讨论】:

              【解决方案10】:

              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
              • [String: String] 根本无法工作,因为有几个数值。它不适用于 Mac Playground
              • @vadian 啊,是的,我没有注意到这些,快速沙箱帮助隐藏了它们! - 现在 [再次] 更正(并在 macOS 上进行了测试)。感谢您指出这一点:)
              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多