Правильный синтаксический анализ JSON в Swift 3


123

Я пытаюсь получить ответ JSON и сохранить результаты в переменной. У меня были версии этого кода, работавшие в предыдущих выпусках Swift, пока не была выпущена GM-версия Xcode 8. Я просмотрел несколько похожих сообщений на StackOverflow: 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]

Можете ли вы поместить образцы данных, возвращенные вашим вызовом API?
User

1
Да, я только что добавил образец результатов, напечатанных после печати (currentConditions). Надеюсь, поможет.
user2563039 04

Разбор json в swift4 с использованием протокола Codable stackoverflow.com/a/52931265/9316566
Насер Мохамед

Ответы:


172

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

'Any' не имеет нижних индексов

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

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

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

Некоторые общие соображения по синтаксическому анализу JSON

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


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

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

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

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

Если корневой объект является array ( []), приведите тип к[[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 непосредственно на структуры / классы.

Например, данный образец 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Целое число может быть декодировано , как Dateс dateDecodingStrategy .secondsSince1970.
  • snaked_cased ключи JSON могут быть преобразованы в camelCase с помощью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"]!к словарю, который компилятор может безопасно вывести для последующей подписки на ключ.
vadian

1
Статья Apple довольно крутая, но по какой-то причине я не мог заставить ее работать с Swift 3. Он жалуется на Type [String: Any]? не имеет нижнего индекса. Были и другие проблемы, но я смог их обойти. У кого-нибудь есть пример их кода, который действительно работает?
Shades

@Shades Задайте вопрос и разместите свой код. Скорее всего, ваша проблема связана с развернутыми опциями.
vadian

Можете ли вы поместить образцы данных, возвращенные вашим вызовом API?
User

12

Большое изменение, которое произошло с 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>

dict!["currently"]! as! [String: String]рухнет
vadian

Точка, в которой он разбился, была ["температура"] !! а также пытаюсь преобразовать
discorevilo

[String: String]вообще не может работать, потому что есть пара числовых значений. Это не работает на игровой площадке 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
}

Он также генерирует код маршалинга без зависимостей, чтобы уговорить возвращаемое значение JSONSerialization.jsonObjectв a Forecast, включая удобный конструктор, который принимает строку JSON, чтобы вы могли быстро проанализировать строго типизированное Forecastзначение и получить доступ к его полям:

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

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


Ссылки не работают.
нарисовал ..

Фантастическая экономия времени! Хорошая работа!
marko

1
Блестящий инструмент. Спасибо.
Ахмет Ардал

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

эй, вы просто добавляете первый метод func loadingJSON (...) в дополнительный быстрый файл или класс. После этого вы сможете вызывать это с любого контроллера в вашем проекте
Марко Вебер,

Я пробовал это, но мне нравится идея продемонстрировать полное решение, а также то, как его использовать, включая вспомогательный метод isConnectedToNetwork (), и мне пришла в голову идея о том, как реализовать его, вероятно, в хорошем коде
Амр Злой,

поэтому я просто использовал новый быстрый файл (левый щелчок по дереву вашего проекта, новый файл ..., быстрый файл) и назвал его jsonhelper.swift. в этот файл вы помещаете первый код, loadingJSON () и isConnectedToNetwork (). после этого вы можете использовать эти две функции в любой части вашего проекта. например, в loginVC в качестве действия кнопки входа в систему вы можете использовать второй код, очевидно, вам нужно изменить домен, строку сообщения и значения paseJson (parseJSON ["loginSuccessfull"]), чтобы они соответствовали ваш php файл
Марко Вебер

0

Swift имеет мощный вывод типов. Давайте избавимся от шаблонов «if let» или «guard let» и принудительно развернем развертывание, используя функциональный подход:

  1. Вот наш JSON. Мы можем использовать необязательный JSON или обычный. В нашем примере я использую optional:

    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.


0

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

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 или 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)
    }
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.