Analizando correctamente JSON en Swift 3

Resuelto user2563039 asked hace 8 años • 11 respuestas

Estoy intentando obtener una respuesta JSON y almacenar los resultados en una variable. He tenido versiones de este código funcionando en versiones anteriores de Swift, hasta que se lanzó la versión GM de Xcode 8. Eché un vistazo a algunas publicaciones similares en StackOverflow: Swift 2 Parsing JSON: no se puede subscribir un valor de tipo 'AnyObject' y JSON Parsing en Swift 3 .

Sin embargo, parece que las ideas allí transmitidas no se aplican en este escenario.

¿Cómo analizo correctamente la respuesta JSON en Swift 3? ¿Ha cambiado algo en la forma en que se lee JSON en Swift 3?

A continuación se muestra el código en cuestión (se puede ejecutar en un patio de juegos):

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

Editar: aquí hay una muestra de los resultados de la llamada API despuésprint(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]
user2563039 avatar Sep 10 '16 13:09 user2563039
Aceptado

En primer lugar, nunca cargue datos de forma sincrónica desde una URL remota , utilice siempre métodos asincrónicos como URLSession.

'Cualquiera' no tiene miembros con subíndice

ocurre porque el compilador no tiene idea de qué tipo son los objetos intermedios (por ejemplo, currentlyen ["currently"]!["temperature"]) y dado que está utilizando tipos de colección Foundation, NSDictionaryel compilador no tiene ninguna idea sobre el tipo.

Además, en Swift 3 es necesario informar al compilador sobre el tipo de todos los objetos con subíndice.

Debe convertir el resultado de la serialización JSON al tipo real.

Este código utiliza URLSessionexclusivamente tipos nativos 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()

Para imprimir todos los pares clave/valor, currentConditionspuede escribir

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

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

Una nota sobre jsonObject(with data:

Muchos (al parecer todos) tutoriales sugieren .mutableContainersopciones .mutableLeavesque son completamente una tontería en Swift. Las dos opciones son opciones heredadas de Objective-C para asignar el resultado a NSMutable...objetos. En Swift, cualquier variable es mutable de forma predeterminada y pasar cualquiera de esas opciones y asignar el resultado a una letconstante no tiene ningún efecto. Además, la mayoría de las implementaciones nunca mutan el JSON deserializado de todos modos.

La única opción (rara) que es útil en Swift es .allowFragmentsla que se requiere si el objeto raíz JSON pudiera ser un tipo de valor ( String, Numbero Bool) nullen lugar de uno de los tipos de colección ( arrayo dictionary). Pero normalmente omite el optionsparámetro que significa Sin opciones .

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

Algunas consideraciones generales para analizar JSON

JSON es un formato de texto bien organizado. Es muy fácil leer una cadena JSON. Lea atentamente la cadena . Sólo hay seis tipos diferentes: dos tipos de colección y cuatro tipos de valor.


Los tipos de colección son

  • Array - JSON: objetos entre corchetes []- Swift: [Any]pero en la mayoría de los casos[[String:Any]]
  • Diccionario - JSON: objetos entre llaves {}- Swift:[String:Any]

Los tipos de valores son

  • Cadena - JSON: cualquier valor entre comillas dobles "Foo", pares "123"o "false"– Swift:String
  • Número - JSON: valores numéricos sin comillas dobles 123o 123.0– Swift: IntoDouble
  • Bool - JSON: trueo false no entre comillas dobles - Swift: trueofalse
  • nulo - JSON: null– Rápido:NSNull

Según la especificación JSON, todas las claves de los diccionarios deben ser String.


Básicamente, siempre se recomienda utilizar enlaces opcionales para desenvolver los opcionales de forma segura.

Si el objeto raíz es un diccionario ( {}), convierta el tipo a[String:Any]

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

y recuperar valores por claves con ( OneOfSupportedJSONTypeses una colección JSON o un tipo de valor como se describe anteriormente).

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

Si el objeto raíz es una matriz ( []), convierta el tipo a[[String:Any]]

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

e iterar a través de la matriz con

for item in parsedData {
    print(item)
}

Si necesita un artículo en un índice específico, verifique también si el índice existe.

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

En el raro caso de que JSON sea simplemente uno de los tipos de valor, en lugar de un tipo de colección, debe pasar la .allowFragmentsopción y convertir el resultado al tipo de valor apropiado, por ejemplo.

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

Apple ha publicado un artículo completo en el Blog de Swift: Trabajar con JSON en Swift


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

En Swift 4+, el Codableprotocolo proporciona una forma más conveniente de analizar JSON directamente en estructuras/clases.

Por ejemplo, el ejemplo JSON proporcionado en la pregunta (ligeramente modificado)

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

se puede decodificar en la estructura Weather. Los tipos de Swift son los mismos que los descritos anteriormente. Hay algunas opciones adicionales:

  • Las cadenas que representan an URLse pueden decodificar directamente como URL.
  • El timenúmero entero se puede decodificar como Datecon el dateDecodingStrategy .secondsSince1970.
  • Las claves JSON de Snaked_cased se pueden convertir a camelCase con elkeyDecodingStrategy .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)
}

Otras fuentes codificables:

  • Apple: codificación y decodificación de tipos personalizados
  • HackingWithSwift: hoja de trucos codificable
  • Ray Wenderlich: codificación y decodificación en Swift
vadian avatar Sep 10 '2016 07:09 vadian

Un gran cambio que ocurrió con Xcode 8 Beta 6 para Swift 3 fue que id ahora se importa como Anyen lugar de AnyObject.

Esto significa que parsedDatase devuelve como un diccionario probablemente con el tipo [Any:Any]. Sin usar un depurador, no podría decirle exactamente qué NSDictionaryhará su conversión, pero el error que está viendo se debe a que dict!["currently"]!tiene tipoAny

Entonces, ¿cómo se soluciona esto? Por la forma en que lo ha referenciado, supongo dict!["currently"]!que es un diccionario y, por lo tanto, tiene muchas opciones:

Primero podrías hacer algo como esto:

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

Esto le dará un objeto de diccionario que luego podrá consultar para obtener valores y así obtener su temperatura de esta manera:

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

O si lo prefieres puedes hacerlo en línea:

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

Espero que esto ayude, me temo que no he tenido tiempo de escribir una aplicación de muestra para probarla.

Una nota final: lo más fácil de hacer podría ser simplemente lanzar la carga útil JSON [String: AnyObject]desde el principio.

let parsedData = try JSONSerialization.jsonObject(with: data as Data, options: .allowFragments) as! Dictionary<String, AnyObject>
discorevilo avatar Sep 10 '2016 07:09 discorevilo
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)")
}
BhuShan PaWar avatar Jan 24 '2017 05:01 BhuShan PaWar

Construí QuickType exactamente para este propósito. Simplemente pegue su JSON de muestra y QuickType generará esta jerarquía de tipos para sus datos 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
}

También genera código de clasificación libre de dependencias para convertir el valor de retorno de JSONSerialization.jsonObjecten a Forecast, incluido un práctico constructor que toma una cadena JSON para que pueda analizar rápidamente un Forecastvalor fuertemente tipado y acceder a sus campos:

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

Puede instalar Quicktype desde npm npm i -g quicktypeo usar la interfaz de usuario web para obtener el código generado completo y pegarlo en su área de juegos.

David Siegel avatar Oct 14 '2017 05:10 David Siegel