Правильно разбор JSON в Swift 3

Я пытаюсь получить ответ JSON и сохранить результаты в переменной. У меня были версии этого кода в предыдущих выпусках Swift, пока не была выпущена GM-версия Xcode 8. Я взглянул на несколько похожих постов по Stackru: Swift 2 Parsing JSON - Невозможно подписать значение типа 'AnyObject' и JSON Parsing в Swift 3.

Однако, похоже, что идеи, изложенные там, не применимы в этом сценарии.

Как правильно разобрать ответ JSON в Swift 3? Что-то изменилось в способе чтения JSON в Swift 3?

Ниже приведен код (его можно запустить на детской площадке):

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]

11 ответов

Решение

Прежде всего, никогда не загружайте данные синхронно с удаленного URL, всегда используйте асинхронные методы, такие как URLSession,

"Любой" не имеет подписчиков

происходит потому, что компилятор понятия не имеет, к какому типу относятся промежуточные объекты (например, currently в ["currently"]!["temperature"]) и так как вы используете типы коллекции Foundation, такие как NSDictionary компилятор понятия не имеет о типе.

Кроме того, в 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. Эти два параметра являются устаревшими параметрами Objective C, чтобы назначить результат NSMutable... объекты. В свифт любой var iable является изменяемым по умолчанию и передает любую из этих опций и присваивает результат let Константа не имеет никакого эффекта вообще. Кроме того, большинство реализаций никогда не изменяют десериализованный JSON.

Единственный (редкий) вариант, который полезен в Swift, это .allowFragments что требуется, если корневой объект JSON может иметь тип значения (String, Number, Bool или же null), а не один из типов коллекции (array или же dictionary). Но обычно опускаем options параметр, который означает, что нет параметров.

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

Некоторые общие соображения для разбора JSON

JSON - это хорошо организованный текстовый формат. Читать строку JSON очень легко. Внимательно прочитайте строку. Существует только шесть различных типов - два типа коллекций и четыре типа значений.


Типы коллекций

  • Array - JSON: объекты в квадратных скобках [] - Свифт: [Any] но в большинстве случаев [[String:Any]]
  • Словарь - JSON: объекты в фигурных скобках {} - Свифт: [String:Any]

Типы значений

  • Строка - JSON: любое значение в двойных кавычках "Foo", четное "123" или же "false" - Свифт: String
  • Число - JSON: числовые значения не в двойных кавычках 123 или же 123.0 - Свифт: Int или же Double
  • Bool - JSON: true или же false не в двойных кавычках - Swift: true или же false
  • нуль - 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: Работа с JSON в Swift

Обновление: в Swift 4+ Codable Протокол предоставляет более удобный способ разбора JSON непосредственно на структуры / классы.

Большое изменение, которое произошло с Xcode 8 Beta 6 для 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>
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)")
}

После этогообновил функцию isConnectToNetwork, благодаря этому сообщению. Проверьте подключение к Интернету с помощью Swift.

я написал дополнительный метод для этого:

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

Я построил 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)

Вы можете установить quicktype из npm с помощью npm i -g quicktype или используйте веб-интерфейс, чтобы получить полный сгенерированный код для вставки на игровую площадку.

Это еще один способ решить вашу проблему. Пожалуйста, ознакомьтесь с решением ниже. Надеюсь, это поможет тебе.

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

2023 | СВИФТ 5.1 | Результаты решения

Образец данных:

      // Codable is important to be able to encode/decode from/to JSON!
struct ConfigCreds: Codable { 
    // some params
}

Пример использования решения:

      var configCreds = ConfigCreds()
var jsonStr: String = ""

// get JSON from Object
configCreds
   .asJson()
   .onSuccess { jsonStr = $0 }
   .onFailure { _ in // any failure code }

// get object of type "ConfigCreds" from JSON
someJsonString
    .decodeFromJson(type: ConfigCreds.self)
    .onSuccess { configCreds = $0 }
    .onFailure { _ in // any failure code }

Серверный код:

      @available(macOS 10.15, *)
public extension Encodable {
    func asJson() -> Result<String, Error>{
        JSONEncoder()
            .try(self)
            .flatMap{ $0.asString() }
    }
}

public extension String {
    func decodeFromJson<T>(type: T.Type) -> Result<T, Error> where T: Decodable {
        self.asData()
            .flatMap { JSONDecoder().try(type, from: $0) }
    }
}

///////////////////////////////
/// HELPERS
//////////////////////////////

@available(macOS 10.15, *)
fileprivate extension JSONEncoder {
    func `try`<T : Encodable>(_ value: T) -> Result<Output, Error> {
        do {
            return .success(try self.encode(value))
        } catch {
            return .failure(error)
        }
    }
}

fileprivate extension JSONDecoder {
    func `try`<T: Decodable>(_ t: T.Type, from data: Data) -> Result<T,Error> {
        do {
            return .success(try self.decode(t, from: data))
        } catch {
            return .failure(error)
        }
    }
}

fileprivate extension String {
    func asData() -> Result<Data, Error> {
        if let data = self.data(using: .utf8) {
            return .success(data)
        } else {
            return .failure(WTF("can't convert string to data: \(self)"))
        }
    }
}

fileprivate extension Data {
    func asString() -> Result<String, Error> {
        if let str = String(data: self, encoding: .utf8) {
            return .success(str)
        } else {
            return .failure(WTF("can't convert Data to string"))
        }
    }
}

fileprivate func WTF(_ msg: String, code: Int = 0) -> Error {
    NSError(code: code, message: msg)
}

internal extension NSError {
    convenience init(code: Int, message: String) {
        let userInfo: [String: String] = [NSLocalizedDescriptionKey:message]
        self.init(domain: "FTW", code: code, userInfo: userInfo)
    }
}

Swift имеет мощный тип вывода. Позволяет избавиться от шаблонов "если позволено" или "охранять пусть" и принудительно разворачивает, используя функциональный подход:

  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

Всего одна строка кода и никаких принудительных развёрток или ручного приведения типов. Этот код работает на детской площадке, поэтому вы можете скопировать и проверить его. Вот реализация на GitHub.

Swift 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)
}
{
    "User":[
      {
        "FirstUser":{
        "name":"John"
        },
       "Information":"XY",
        "SecondUser":{
        "name":"Tom"
      }
     }
   ]
}

Если я создаю модель с использованием предыдущего json. Используя эту ссылку [блог]: http://www.jsoncafe.com/ для создания структуры Codable или Any Format

Модель

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

Проблема связана с методом взаимодействия 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()
Другие вопросы по тегам