Decodificando json com Swift 4 Parte 1

Decodificando json com Swift 4 Parte 1

Já abordamos anteriormente aqui no blog sobre como pode ser trabalhoso decodificar arquivos json com o Swift e como bibliotecas de terceiros podem nos ajudar com esta tarefa. O artigo pode ser lido aqui. No Swift 4 foi acrescentado uma forma de lidarmos com json de maneira simples e nativa. Neste artigo iremos examinar alguns pontos desta novidade.

O básico

Para o seguinte fonte json:

{
  "temp": 299.38,
  "pressure": 1016,
  "humidity": 65,
  "temp_min": 298.15,
  "temp_max": 300.15
}

Temos a estrutura a seguir:

struct WeatherResult {
  let temp: Double
  let pressure: Int
  let humidity: Double
  let temp_min: Double
  let temp_max: Double
}

O Swift 4 acrescentou a funcionalidade de codificar e decodificar arquivos json com os protocolos Encodable e Decodable. O protocolo Codable engloba os dois protocolos anteriores e pode ser utilizado para os dois casos.

typealias Codable = Decodable & Encodable

Adicionando o protocolo a nossa struct.

struct WeatherResult: Codable {
  ...
}

Para decodificar o json basta criarmos um JSONDecoder.

let json = stringJson.data(using: .utf8)!
let decoder = JSONDecoder()
let weather = try! decoder.decode(Weather.self, from: json)

Apenas implementando o protocolo Codable tivemos o nosso parse de graça. Utilizei force unwrapping para simplificar o exemplo.
Para codificar o json a partir de um objeto basta implementarmos o protocolo Codable e utilizar o JSONEncoder.

let newWeather = Weather(temp: 295.15, pressure: 1016, humidity: 80, temp_min: 291.15, temp_max: 296.15)

let encoder = JSONEncoder()
let newWeatherData = try! encoder.encode(newWeather)
let newJsonString = String(data: newWeatherData, encoding: .utf8)

O resultado será:

{"humidity":80,"temp_max":296.14999999999998,"temp_min":291.14999999999998,"temp":295.14999999999998,"pressure":1016}

Se quisermos uma saída mais amigável a leitura podemos utilizar o a propriedade outputFormatting do JSONEncoder com o valor .prettyPrinted. Por padrão ela vem com o valor .compact.

encoder.outputFormatting = .prettyPrinted

Neste caso o resultado será:

{
  "humidity" : 80,
  "temp_max" : 296.14999999999998,
  "temp_min" : 291.14999999999998,
  "temp" : 295.14999999999998,
  "pressure" : 1016
}

Nomes de chaves personalizados

Teremos situações em que utilizaremos tipos diferentes de nomenclatura de propriedades e chaves json. Neste caso o protocolo CodingKey nos permite customizar o mapeamento da propriedade da struct ou classe com a chave do json. Para implementarmos o protocolo teremos que criar um enum CodingKeys do tipo String com o nome das propriedades e o valor da chave no json.
Utilizando o json anterior, temos:

struct Weather: Codable {
  let temperature: Double
  let pressure: Int
  let humidity: Double
  let minimumTemperature: Double
  let maximumTemperature: Double

  enum CodingKeys: String, CodingKey {
    case temperature = "temp"
    case pressure, humidity
    case minimumTemperature = "temp_min"
    case maximumTemperature = "temp_max"
  }
}

Definindo o enum CodingKeys o Swift será capaz de decodificar o json para as respectivas propriedades do objeto.

Swift 4.1 e keyDecodingStrategy

No Swift 4.1 foi introduzida uma nova propriedade ao protocolo Codable que permite converter nomes do padrão camelCase para snake_case e vice versa. A propriedade chama-se keyDecodingStrategy. Por padrão ela utiliza o valor .useDefaultKeys que utiliza os mesmos nomes para a propriedade do objeto e para chave no arquivo json. Mas se a alterarmos para .convertFromSnakeCase teremos a conversão do padrão snake_case utilizado no json para o padrão camelCase utilizado no Swift.
Considerando o json:

{
  "temp": 299.38,
  "pressure": 1016,
  "humidity": 65,
  "temp_min": 298.15,
  "temp_max": 300.15
}

E a struct:

struct Weather: Codable {
  let temp: Double
  let pressure: Int
  let humidity: Double
  let tempMin: Double
  let tempMax: Double
}

Podemos decodificar o arquivo com o código abaixo.

let json = stringJson.data(using: .utf8)!
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let weather = try! decoder.decode(Weather.self, from: json)

Esta melhoria torna ainda mais simples a utilização de arquivo json com o Swift. A codificação de objeto para json funciona da mesma forma só que iremos utilizar o valor .convertToSnakeCase no JSONEncoder.
Para o objeto weather a seguir.

struct Weather: Encodable {
  let temp: Double
  let pressure: Int
  let humidity: Double
  let tempMin: Double
  let tempMax: Double
}

let newWeather = Weather(temp: 295.15, pressure: 1016, humidity: 80, tempMin: 291.15, tempMax: 296.15)

Podemos codificar com o seguinte código:

let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
let newWeatherData = try! encoder.encode(newWeather)
let newJsonString = String(data: newWeatherData, encoding: .utf8)

O resultado será:

"{
  "humidity": 80,
  "temp_max": 296.15,
  "temp_min": 291.15,
  "temp": 295.15,
  "pressure": 1016
}"

Além destas opções você pode implementar uma solução customizada utilizando o valor .custom conforme o exemplo abaixo retirado do fonte de testes do Swift.

let customKeyConversion = { (_ path: [CodingKey]) -> CodingKey in
  // This converter removes the first 4 characters from the start of all string keys, if it has more than 4 characters
  let string = path.last!.stringValue
  guard string.count > 4 else { return path.last! }
  let newString = string.substring(from: string.index(string.startIndex, offsetBy: 4, limitedBy: string.endIndex)!)
  return _TestKey(stringValue: newString)!
}

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .custom(customKeyConversion)

O código completo pode ser encontrado aqui.

Formatando datas

Podemos alterar a forma como o Swift trata as datas do json através da propriedade .dateDecodingStrategy. Por padrão ela contém o valor .deferredToDate e podemos escolher entre os valores .iso8601, .millisecondsSince1970, .secondsSince1970. Também podemos utilizar os valores .formatted(DateFormatter), que recebe um DateFormatter e o valor .custom((Decoder) throws -> Date) para situações bem específicas.
No JSONEncoder funciona da mesma forma. Só que a propriedade é a .dateEncodingStrategy e o valor custom tem a assinatura .custom((Date, Encoder) throws -> Void).

Formatando números decimais

Existem raros casos onde os valores numéricos podem vir com valores tais como “Infinity” ou “NaN”. Como o Swift não pode lidar com estes valores de forma automática ele irá gerar uma exceção. Para contornar este problema devemos alterar a propriedade nonConformingFloatDecodingStrategy do JSONDecoder. Por padrão ela terá o valor .throw. Mas podemos customizá-la com .convertFromString(positiveInfinity:,negativeInfinity:,nan:).Exemplo:

decoder.nonConformingFloatDecodingStrategy = .convertFromString(positiveInfinity: "Infinity", negativeInfinity: "-Infinity", nan: "NaN")

Podemos fazer a mesma coisa com o JSONEncoder mas utilizando nonConformingFloatEncodingStrategy.

Formatando Dados

É bem comum vermos strings no formato base64 em dados json mas, caso os dados venham em um formato diferente você pode utilizar a propriedade dataDecodingStrategy para informar o tipo de dados enviado. As opções são: .base64 que é o valor padrão e .custom((Decoder) throws -> Data).
Da mesma forma no JSONEncoder só que a propriedade é a dataEncodingStrategy com as opções .base64 que é a padrão e .custom((Data, Encoder) throws -> Void).

Decodificando estruturas

A decodificação de json também funciona com arrays e dicionários. Caso a estrutura raiz seja um array conforme o json abaixo.

[
  {
    "id": 3470127,
    "name": "Belo Horizonte",
    "temp": 26.91,
    "pressure": 1012,
    "humidity": 51,
    "temp_min": 24,
    "temp_max": 30
  }
]

Podemos decodificar-lo passando um array como tipo para o JSONDecoder.

struct Weather: Codable {
  let id: Int
  let name: String
  let temp: Double
  let pressure: Int
  let humidity: Double
  let temp_min: Double
  let temp_max: Double
}

let json = stringJson.data(using: .utf8)!
let decoder = JSONDecoder()
let weather = try! decoder.decode([Weather].self, from: json)

Um arquivo json pode ter estruturas mais complexas como um cabeçalho ou com várias estruturas diferentes. O JSONDecoder funciona com todas elas desde que todos os tipos implementem o protocolo Codable. A seguir temos um exemplo de uma resposta de uma API utilizada no post Usando Unbox para decodificar JSON.

{
  "message": "accurate",
  "cod": "200",
  "count": 1,
  "list": [
    {
      "id": 3470127,
      "name": "Belo Horizonte",
      "coord": {
        "lat": -19.9228,
        "lon": -43.9451
      },
      "main": {
        "temp": 26.91,
        "pressure": 1012,
        "humidity": 51,
        "temp_min": 24,
        "temp_max": 30
      },
      "dt": 1519236000,
      "wind": {
        "speed": 3.6,
        "deg": 340
      },
      "sys": {
        "country": "BR"
      },
      "rain": null,
      "snow": null,
      "clouds": {
        "all": 40
      },
      "weather": [
        {
          "id": 211,
          "main": "Thunderstorm",
          "description": "trovoada",
          "icon": "11d"
        },
        {
          "id": 521,
          "main": "Rain",
          "description": "chuva",
          "icon": "09d"
        }
      ]
    }
  ]
}

Podemos decodificar o json acima com o seguinte código:

struct WeatherResponse: Codable {
  let message: String
  let cod: String
  let count: Int
  let list: [WeatherResult]

  struct WeatherResult: Codable {
    let id: Int
    let name: String
    let coord: Coordinates
    let main: Measures
    let dt: Date
    let wind: Wind
    let sys: System
    let rain: String?
    let snow: String?
    let clouds: Clouds
    let weather: [Weather]

    struct Coordinates: Codable {
      let lat: Double
      let lon: Double
    }

    struct Weather: Codable {
      let id: Int
      let main: String
      let description: String
      let icon: String
    }

    struct Measures: Codable {
      let temp: Double
      let pressure: Int
      let humidity: Double
      let temp_min: Double
      let temp_max: Double
    }

    struct Wind: Codable {
      let speed: Double
      let deg: Int
    }

    struct Clouds: Codable {
      let all: Int
    }

    struct System: Codable {
      let country: String
    }
  }
}

let json = stringJson.data(using: .utf8)!
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .secondsSince1970
let weather = try! decoder.decode(WeatherResponse.self, from: json)

Apenas acrescentando o protocolo Codable ao nosso model obtivemos a decodificação do json de graça. No Swift 3 teríamos que escrever muito código pra conseguir o mesmo resultado.

Custom Decode

Caso precise realizar alguma operação como por exemplo alterar a estrutura da informação pra salvar no model você pode utilizar o initializer do protocol Codable. Por exemplo podemos armazenar as informações que estão na estrutura main do json no primeiro nível do model.

{
  "id": 3470127,
  "name": "Belo Horizonte",
  "main": {
    "temp": 26.91,
    "pressure": 1012,
    "humidity": 51,
    "temp_min": 24,
    "temp_max": 30
  }
}

Pra isto devemos criar um enum CodingKey com os campos da estrutura main.

struct Weather: Codable {
  let id: Int
  let name: String
  let temperature: Double
  let pressure: Int
  let humidity: Double
  let minimumTemperature: Double
  let maximumTemperature: Double
  let rain: String?
  let snow: String?

  private let main: Int8? = nil

  enum mainCodingKeys: String, CodingKey {
    case temperature = "temp"
    case pressure, humidity
    case minimumTemperature = "temp_min"
    case maximumTemperature = "temp_max"
  }
}

Criei uma propriedade main porque o protocolo exige que todos os valores do enum tenham uma propriedade correspondente. Agora vamos implementar o initializer init(from:) throws. Pra isto utilizamos o decoder para criar um “container” que é uma coleção de valores que podem ser:

  • Keyed Container: um dicionário de valores com as chaves do CodingKeys;
  • Unkeyed Container: um array ordenado com os valores;
  • Single Value Container: o valor sem nenhum tipo de container.

No nosso caso iremos utilizar a função container(keyedBy:) que retorno um KeyedDecodingContainer que iremos utilizar para acessar os valores. Podemos destacar os seguintes métodos no KeyedDecodingContainer:

  • contains(_:) – recebe uma chave e retorna true caso a mesma exista no container;
  • decode(_:forKey:) recebe o tipo do valor e a chave. Retorna o valor da chave passada e caso o tipo seja diferente do esperado gera a exceção DecodingError.TypeMismatch.
  • decodeIfPresent(_:forKey:) – mesmo funcionamento do método decode(_:forKey:) só que não gera a exceção DecodingError.keyNotFound caso a chave não exista no json.
  • nestedContainer(keyedBy:forKey:) – recebe um enum que implemente o protocolo CodingKey com as chaves esperadas e o nome da chave que armazena a estrutura aninhada. Retorna um KeyedDecodingContainer com as chaves e os respectivos valores.
  • nestedUnkeyedContainer(forKey:) – recebe o nome da chave que armazena a estrutura aninhada. Retorna um UnkeyedDecodingContainer com os valores.

O nosso initializers ficará:

struct Weather: Codable {
  ...

  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)

    id = try container.decode(Int.self, forKey: .id)
    name = try container.decode(String.self, forKey: .name)
    rain = try container.decodeIfPresent(String.self, forKey: .rain)
    snow = try container.decodeIfPresent(String.self, forKey: .snow)

    let main = try container.nestedContainer(keyedBy: mainCodingKeys.self, forKey: .main)
    temperature = try main.decode(Double.self, forKey: .temperature)
    pressure = try main.decode(Int.self, forKey: .pressure)
    humidity = try main.decode(Double.self, forKey: .humidity)
    minimumTemperature = try main.decode(Double.self, forKey: .minimumTemperature)
    maximumTemperature = try main.decode(Double.self, forKey: .maximumTemperature)
  }
}

Com a nossa struct pronta podemos decodificar o json.

do {
  let json = stringJson.data(using: .utf8)!
  let decoder = JSONDecoder()
  decoder.dateDecodingStrategy = .secondsSince1970
  let weather = try decoder.decode(Weather.self, from: json)
} catch DecodingError.dataCorrupted(let context) {
  print(context.debugDescription)
} catch DecodingError.keyNotFound(let codingKey, let context) {
  print("Key: \(codingKey). \(context.debugDescription)")
} catch DecodingError.typeMismatch(_, let context) {
  print(context.debugDescription)
} catch {
  print(error.localizedDescription)
}

Existem outras funcionalidades interessantes sobre os protocolos Encodable e Decodable que não foram tratadas neste artigo tais como herança e outras funcionalidades relacionadas a custom types. O Ben Scheirman fez o ótimo artigo Ultimate Guide to JSON Parsing with Swift 4 sobre o assunto que contém alguns destes tópicos. Mais informações podem ser encontradas na documentação do Xcode.

A seguir iremos criar um projeto pra aplicarmos as informações que vimos até agora. A segunda parte do artigo pode ser encontrada aqui.

Espero que tenham gostado do artigo. Qualquer dúvida, crítica ou sugestão de novos assuntos deixe um comentário abaixo. Caso queira entrar em contato pode me chamar no twitter @mateusfsilva. Se gostou compartilhe.

2 comentários em “Decodificando json com Swift 4 Parte 1

Deixe um comentário