Como faço para usar chaves personalizadas com o protocolo decodificável do Swift 4?

102

O Swift 4 introduziu suporte para codificação e decodificação JSON nativa por meio do Decodableprotocolo. Como faço para usar chaves personalizadas para isso?

Por exemplo, digamos que eu tenha uma estrutura

struct Address:Codable {
    var street:String
    var zip:String
    var city:String
    var state:String
}

Posso codificar isso para JSON.

let address = Address(street: "Apple Bay Street", zip: "94608", city: "Emeryville", state: "California")

if let encoded = try? encoder.encode(address) {
    if let json = String(data: encoded, encoding: .utf8) {
        // Print JSON String
        print(json)

        // JSON string is 
           { "state":"California", 
             "street":"Apple Bay Street", 
             "zip":"94608", 
             "city":"Emeryville" 
           }
    }
}

Posso codificar isso de volta para um objeto.

    let newAddress: Address = try decoder.decode(Address.self, from: encoded)

Mas se eu tivesse um objeto json que fosse

{ 
   "state":"California", 
   "street":"Apple Bay Street", 
   "zip_code":"94608", 
   "city":"Emeryville" 
}

Como eu diria para o decodificador Addressdesse zip_codemapa zip? Acredito que você use o novo CodingKeyprotocolo, mas não consigo descobrir como usá-lo.

chrismanderson
fonte

Respostas:

258

Personalização manual das chaves de codificação

Em seu exemplo, você está obtendo uma conformidade gerada automaticamente para, Codablejá que todas as suas propriedades também estão em conformidade Codable. Essa conformidade cria automaticamente um tipo de chave que simplesmente corresponde aos nomes das propriedades - que é então usado para codificar / decodificar a partir de um único contêiner com chave.

No entanto, uma característica realmente interessante desta conformidade gerada automaticamente é que se você definir um aninhado enumem seu tipo chamado " CodingKeys" (ou usar um typealiascom este nome) que está em conformidade com oCodingKey protocolo - o Swift usará isso automaticamente como o tipo de chave. Isso, portanto, permite que você personalize facilmente as chaves com as quais suas propriedades são codificadas / decodificadas.

Então, isso significa que você pode apenas dizer:

struct Address : Codable {

    var street: String
    var zip: String
    var city: String
    var state: String

    private enum CodingKeys : String, CodingKey {
        case street, zip = "zip_code", city, state
    }
}

Os nomes dos casos enum precisam corresponder aos nomes das propriedades, e os valores brutos desses casos precisam corresponder às chaves para as quais você está codificando / decodificando (a menos que especificado de outra forma, os valores brutos de um String enumeração serão iguais aos nomes de caso ) Portanto, a zippropriedade agora será codificada / decodificada usando a chave "zip_code".

As regras exatas para a autogeração Encodable/ Decodableconformidade são detalhadas por na proposta de evolução (grifo meu):

Além da CodingKeysíntese automática de requisitos para enums, Encodablee os Decodablerequisitos também podem ser sintetizados automaticamente para certos tipos:

  1. Os tipos em conformidade com as Encodablepropriedades de cada Encodableum Stringobtêm CodingKeypropriedades de mapeamento enum geradas automaticamente para nomes de caso. Da mesma forma para Decodabletipos cujas propriedades são todasDecodable

  2. Tipos que se enquadram em (1) - e tipos que fornecem manualmente um CodingKey enum(nomeado CodingKeys, diretamente ou via a typealias) cujos casos mapeiam 1-para-1 para Encodable/ Decodablepropriedades por nome - obtêm síntese automática deinit(from:) e encode(to:)conforme apropriado, usando essas propriedades e chaves

  3. Tipos que não se enquadram em (1) nem (2) terão que fornecer um tipo de chave personalizado se necessário e fornecer o seu próprio init(from:)e encode(to:), conforme apropriado

Codificação de exemplo:

import Foundation

let address = Address(street: "Apple Bay Street", zip: "94608",
                      city: "Emeryville", state: "California")

do {
    let encoded = try JSONEncoder().encode(address)
    print(String(decoding: encoded, as: UTF8.self))
} catch {
    print(error)
}
//{"state":"California","street":"Apple Bay Street","zip_code":"94608","city":"Emeryville"}

Exemplo de decodificação:

// using the """ multi-line string literal here, as introduced in SE-0168,
// to avoid escaping the quotation marks
let jsonString = """
{"state":"California","street":"Apple Bay Street","zip_code":"94608","city":"Emeryville"}
"""

do {
    let decoded = try JSONDecoder().decode(Address.self, from: Data(jsonString.utf8))
    print(decoded)
} catch {
    print(error)
}

// Address(street: "Apple Bay Street", zip: "94608",
// city: "Emeryville", state: "California")

snake_caseChaves JSON automáticas para camelCasenomes de propriedades

No Swift 4.1, se você renomear sua zippropriedade para zipCode, poderá aproveitar as vantagens das estratégias de codificação / decodificação de chave em JSONEncodere JSONDecoderpara converter automaticamente as chaves de codificação entre camelCasee snake_case.

Codificação de exemplo:

import Foundation

struct Address : Codable {
  var street: String
  var zipCode: String
  var city: String
  var state: String
}

let address = Address(street: "Apple Bay Street", zipCode: "94608",
                      city: "Emeryville", state: "California")

do {
  let encoder = JSONEncoder()
  encoder.keyEncodingStrategy = .convertToSnakeCase
  let encoded = try encoder.encode(address)
  print(String(decoding: encoded, as: UTF8.self))
} catch {
  print(error)
}
//{"state":"California","street":"Apple Bay Street","zip_code":"94608","city":"Emeryville"}

Exemplo de decodificação:

let jsonString = """
{"state":"California","street":"Apple Bay Street","zip_code":"94608","city":"Emeryville"}
"""

do {
  let decoder = JSONDecoder()
  decoder.keyDecodingStrategy = .convertFromSnakeCase
  let decoded = try decoder.decode(Address.self, from: Data(jsonString.utf8))
  print(decoded)
} catch {
  print(error)
}

// Address(street: "Apple Bay Street", zipCode: "94608",
// city: "Emeryville", state: "California")

Uma coisa importante a notar sobre esta estratégia, no entanto, é que ela não será capaz de percorrer alguns nomes de propriedade com acrônimos ou iniciais que, de acordo com as diretrizes de design da API Swift , devem ser uniformemente maiúsculas ou minúsculas (dependendo da posição )

Por exemplo, uma propriedade nomeada someURLserá codificada com a chave some_url, mas na decodificação, será transformada em someUrl.

Para corrigir isso, você terá que especificar manualmente a chave de codificação dessa propriedade para ser a string que o decodificador espera, por exemplo someUrl, neste caso (que ainda será transformada some_urlpelo codificador):

struct S : Codable {

  private enum CodingKeys : String, CodingKey {
    case someURL = "someUrl", someOtherProperty
  }

  var someURL: String
  var someOtherProperty: String
}

(Isso não responde estritamente à sua pergunta específica, mas dada a natureza canônica desta sessão de perguntas e respostas, acho que vale a pena incluí-la)

Mapeamento de teclas JSON automático personalizado

No Swift 4.1, você pode tirar proveito das estratégias de codificação / decodificação de chave personalizada em JSONEncodere JSONDecoder, permitindo que você forneça uma função personalizada para mapear chaves de codificação.

A função que você fornece leva um [CodingKey] , que representa o caminho de codificação para o ponto atual na codificação / decodificação (na maioria dos casos, você só precisa considerar o último elemento; ou seja, a chave atual). A função retorna um CodingKeyque substituirá a última chave nesta matriz.

Por exemplo, UpperCamelCasechaves JSON para lowerCamelCasenomes de propriedades:

import Foundation

// wrapper to allow us to substitute our mapped string keys.
struct AnyCodingKey : CodingKey {

  var stringValue: String
  var intValue: Int?

  init(_ base: CodingKey) {
    self.init(stringValue: base.stringValue, intValue: base.intValue)
  }

  init(stringValue: String) {
    self.stringValue = stringValue
  }

  init(intValue: Int) {
    self.stringValue = "\(intValue)"
    self.intValue = intValue
  }

  init(stringValue: String, intValue: Int?) {
    self.stringValue = stringValue
    self.intValue = intValue
  }
}

extension JSONEncoder.KeyEncodingStrategy {

  static var convertToUpperCamelCase: JSONEncoder.KeyEncodingStrategy {
    return .custom { codingKeys in

      var key = AnyCodingKey(codingKeys.last!)

      // uppercase first letter
      if let firstChar = key.stringValue.first {
        let i = key.stringValue.startIndex
        key.stringValue.replaceSubrange(
          i ... i, with: String(firstChar).uppercased()
        )
      }
      return key
    }
  }
}

extension JSONDecoder.KeyDecodingStrategy {

  static var convertFromUpperCamelCase: JSONDecoder.KeyDecodingStrategy {
    return .custom { codingKeys in

      var key = AnyCodingKey(codingKeys.last!)

      // lowercase first letter
      if let firstChar = key.stringValue.first {
        let i = key.stringValue.startIndex
        key.stringValue.replaceSubrange(
          i ... i, with: String(firstChar).lowercased()
        )
      }
      return key
    }
  }
}

Agora você pode codificar com a .convertToUpperCamelCaseestratégia principal:

let address = Address(street: "Apple Bay Street", zipCode: "94608",
                      city: "Emeryville", state: "California")

do {
  let encoder = JSONEncoder()
  encoder.keyEncodingStrategy = .convertToUpperCamelCase
  let encoded = try encoder.encode(address)
  print(String(decoding: encoded, as: UTF8.self))
} catch {
  print(error)
}
//{"Street":"Apple Bay Street","City":"Emeryville","State":"California","ZipCode":"94608"}

e decodifique com a .convertFromUpperCamelCaseestratégia principal:

let jsonString = """
{"Street":"Apple Bay Street","City":"Emeryville","State":"California","ZipCode":"94608"}
"""

do {
  let decoder = JSONDecoder()
  decoder.keyDecodingStrategy = .convertFromUpperCamelCase
  let decoded = try decoder.decode(Address.self, from: Data(jsonString.utf8))
  print(decoded)
} catch {
  print(error)
}

// Address(street: "Apple Bay Street", zipCode: "94608",
// city: "Emeryville", state: "California")
Hamish
fonte
Apenas descobri isso sozinho! Eu me pergunto, há uma maneira de substituir apenas uma chave que desejo alterar e deixar o resto sozinho? Por exemplo, na declaração case, no CodingKeysenum; posso apenas listar a chave que estou mudando?
chrismanderson
2
"""é para um literal de várias linhas :)
Martin R
6
@MartinR Ou mesmo apenas uma única linha literal sem ter que escapar "s: D
Hamish
1
@chrismanderson Exatamente - especialmente considerando que o compilador impõe que os nomes dos casos sejam mantidos em sincronia com os nomes das propriedades (isso apresentará um erro dizendo que você não está em conformidade com o Codablecontrário)
Hamish
1
@ClayEllis Ah sim, embora, é claro, usar os contêineres aninhados, por exemplo, diretamente no inicializador ou Addressdesnecessariamente vincule-se à decodificação de um objeto JSON que começa em um local específico no gráfico do objeto pai. Seria muito melhor abstrair o caminho da chave inicial até o próprio decodificador - aqui está uma implementação tosca de hackey .
Hamish de
17

Com o Swift 4.2, de acordo com suas necessidades, você pode usar uma das 3 estratégias a seguir para fazer com que os nomes das propriedades personalizadas de seus objetos de modelo correspondam às suas chaves JSON.


# 1. Usando chaves de codificação personalizadas

Quando você declara uma estrutura que está em conformidade com Codable( Decodablee Encodableprotocolos) com a implementação a seguir ...

struct Address: Codable {
    var street: String
    var zip: String
    var city: String
    var state: String        
}

... o compilador gera automaticamente um enum aninhado que está em conformidade com o CodingKeyprotocolo para você.

struct Address: Codable {
    var street: String
    var zip: String
    var city: String
    var state: String

    // compiler generated
    private enum CodingKeys: String, CodingKey {
        case street
        case zip
        case city
        case state
    }
}

Portanto, se as chaves usadas em seu formato de dados serializados não corresponderem aos nomes de propriedade de seu tipo de dados, você pode implementar manualmente este enum e definir o apropriado rawValue para os casos necessários.

O exemplo a seguir mostra como fazer:

import Foundation

struct Address: Codable {
    var street: String
    var zip: String
    var city: String
    var state: String

    private enum CodingKeys: String, CodingKey {
        case street
        case zip = "zip_code"
        case city
        case state
    }
}

Codificar (substituindo a zippropriedade pela chave JSON "zip_code"):

let address = Address(street: "Apple Bay Street", zip: "94608", city: "Emeryville", state: "California")

let encoder = JSONEncoder()
if let jsonData = try? encoder.encode(address), let jsonString = String(data: jsonData, encoding: .utf8) {
    print(jsonString)
}

/*
 prints:
 {"state":"California","street":"Apple Bay Street","zip_code":"94608","city":"Emeryville"}
 */

Decodificar (substituindo a chave JSON "zip_code" pela zippropriedade):

let jsonString = """
{"state":"California","street":"Apple Bay Street","zip_code":"94608","city":"Emeryville"}
"""

let decoder = JSONDecoder()
if let jsonData = jsonString.data(using: .utf8), let address = try? decoder.decode(Address.self, from: jsonData) {
    print(address)
}

/*
 prints:
 Address(street: "Apple Bay Street", zip: "94608", city: "Emeryville", state: "California")
 */

# 2. Usando a capa de cobra para estratégias de codificação de chave de capa de camelo

Se o seu JSON tem teclas encaixotado-cobra e quer convertê-los em propriedades encaixotado-camelo para o seu modelo de objeto, você pode definir o seu JSONEncoder's keyEncodingStrategye JSONDecoder' s keyDecodingStrategypropriedades para .convertToSnakeCase.

O exemplo a seguir mostra como fazer:

import Foundation

struct Address: Codable {
    var street: String
    var zipCode: String
    var cityName: String
    var state: String
}

Codificar (convertendo propriedades com caixa de camelo em chaves JSON com caixa de cobra):

let address = Address(street: "Apple Bay Street", zipCode: "94608", cityName: "Emeryville", state: "California")

let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
if let jsonData = try? encoder.encode(address), let jsonString = String(data: jsonData, encoding: .utf8) {
    print(jsonString)
}

/*
 prints:
 {"state":"California","street":"Apple Bay Street","zip_code":"94608","city_name":"Emeryville"}
 */

Decodificar (converter chaves JSON com caixa de cobra em propriedades com caixa de camelo):

let jsonString = """
{"state":"California","street":"Apple Bay Street","zip_code":"94608","city_name":"Emeryville"}
"""

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
if let jsonData = jsonString.data(using: .utf8), let address = try? decoder.decode(Address.self, from: jsonData) {
    print(address)
}

/*
 prints:
 Address(street: "Apple Bay Street", zipCode: "94608", cityName: "Emeryville", state: "California")
 */

# 3. Usando estratégias de codificação de chaves personalizadas

Se necessário, JSONEncodere JSONDecoderpermitir que você defina uma estratégia personalizada para mapear as chaves de codificação usando JSONEncoder.KeyEncodingStrategy.custom(_:)e JSONDecoder.KeyDecodingStrategy.custom(_:).

O exemplo a seguir mostra como implementá-los:

import Foundation

struct Address: Codable {
    var street: String
    var zip: String
    var city: String
    var state: String
}

struct AnyKey: CodingKey {
    var stringValue: String
    var intValue: Int?

    init?(stringValue: String) {
        self.stringValue = stringValue
    }

    init?(intValue: Int) {
        self.stringValue = String(intValue)
        self.intValue = intValue
    }
}

Codificar (convertendo as propriedades da primeira letra minúscula em chaves JSON da primeira letra maiúscula):

let address = Address(street: "Apple Bay Street", zip: "94608", city: "Emeryville", state: "California")

let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .custom({ (keys) -> CodingKey in
    let lastKey = keys.last!
    guard lastKey.intValue == nil else { return lastKey }
    let stringValue = lastKey.stringValue.prefix(1).uppercased() + lastKey.stringValue.dropFirst()
    return AnyKey(stringValue: stringValue)!
})

if let jsonData = try? encoder.encode(address), let jsonString = String(data: jsonData, encoding: .utf8) {
    print(jsonString)
}

/*
 prints:
 {"Zip":"94608","Street":"Apple Bay Street","City":"Emeryville","State":"California"}
 */

Decodificar (convertendo as chaves JSON da primeira letra maiúscula em propriedades da primeira letra minúscula):

let jsonString = """
{"State":"California","Street":"Apple Bay Street","Zip":"94608","City":"Emeryville"}
"""

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .custom({ (keys) -> CodingKey in
    let lastKey = keys.last!
    guard lastKey.intValue == nil else { return lastKey }
    let stringValue = lastKey.stringValue.prefix(1).lowercased() + lastKey.stringValue.dropFirst()
    return AnyKey(stringValue: stringValue)!
})

if let jsonData = jsonString.data(using: .utf8), let address = try? decoder.decode(Address.self, from: jsonData) {
    print(address)
}

/*
 prints:
 Address(street: "Apple Bay Street", zip: "94608", city: "Emeryville", state: "California")
 */

Fontes:

Imanou Petit
fonte
3

O que fiz é criar a própria estrutura, exatamente como o que você está obtendo do JSON com relação aos seus tipos de dados.

Bem assim:

struct Track {
let id : Int
let contributingArtistNames:String
let name : String
let albumName :String
let copyrightP:String
let copyrightC:String
let playlistCount:Int
let trackPopularity:Int
let playlistFollowerCount:Int
let artistFollowerCount : Int
let label : String
}

Depois disso você precisa criar uma extensão da mesma structextensão decodablee enumda mesma estrutura com CodingKeye então você precisa inicializar o decodificador usando este enum com suas chaves e tipos de dados (as chaves virão do enum e os tipos de dados virão ou dirão referenciado da própria estrutura)

extension Track: Decodable {

    enum TrackCodingKeys: String, CodingKey {
        case id = "id"
        case contributingArtistNames = "primaryArtistsNames"
        case spotifyId = "spotifyId"
        case name = "name"
        case albumName = "albumName"
        case albumImageUrl = "albumImageUrl"
        case copyrightP = "copyrightP"
        case copyrightC = "copyrightC"
        case playlistCount = "playlistCount"
        case trackPopularity = "trackPopularity"
        case playlistFollowerCount = "playlistFollowerCount"
        case artistFollowerCount = "artistFollowers"
        case label = "label"
    }
    init(from decoder: Decoder) throws {
        let trackContainer = try decoder.container(keyedBy: TrackCodingKeys.self)
        if trackContainer.contains(.id){
            id = try trackContainer.decode(Int.self, forKey: .id)
        }else{
            id = 0
        }
        if trackContainer.contains(.contributingArtistNames){
            contributingArtistNames = try trackContainer.decode(String.self, forKey: .contributingArtistNames)
        }else{
            contributingArtistNames = ""
        }
        if trackContainer.contains(.spotifyId){
            spotifyId = try trackContainer.decode(String.self, forKey: .spotifyId)
        }else{
            spotifyId = ""
        }
        if trackContainer.contains(.name){
            name = try trackContainer.decode(String.self, forKey: .name)
        }else{
            name = ""
        }
        if trackContainer.contains(.albumName){
            albumName = try trackContainer.decode(String.self, forKey: .albumName)
        }else{
            albumName = ""
        }
        if trackContainer.contains(.albumImageUrl){
            albumImageUrl = try trackContainer.decode(String.self, forKey: .albumImageUrl)
        }else{
            albumImageUrl = ""
        }
        if trackContainer.contains(.copyrightP){
            copyrightP = try trackContainer.decode(String.self, forKey: .copyrightP)
        }else{
            copyrightP = ""
        }
        if trackContainer.contains(.copyrightC){
                copyrightC = try trackContainer.decode(String.self, forKey: .copyrightC)
        }else{
            copyrightC = ""
        }
        if trackContainer.contains(.playlistCount){
            playlistCount = try trackContainer.decode(Int.self, forKey: .playlistCount)
        }else{
            playlistCount = 0
        }

        if trackContainer.contains(.trackPopularity){
            trackPopularity = try trackContainer.decode(Int.self, forKey: .trackPopularity)
        }else{
            trackPopularity = 0
        }
        if trackContainer.contains(.playlistFollowerCount){
            playlistFollowerCount = try trackContainer.decode(Int.self, forKey: .playlistFollowerCount)
        }else{
            playlistFollowerCount = 0
        }

        if trackContainer.contains(.artistFollowerCount){
            artistFollowerCount = try trackContainer.decode(Int.self, forKey: .artistFollowerCount)
        }else{
            artistFollowerCount = 0
        }
        if trackContainer.contains(.label){
            label = try trackContainer.decode(String.self, forKey: .label)
        }else{
            label = ""
        }
    }
}

Você precisa alterar aqui cada chave e tipo de dados de acordo com suas necessidades e usá-los com o decodificador.

Tushar
fonte
-1

Ao usar CodingKey, você pode usar chaves personalizadas em protocolo codificável ou decodificável.

struct person: Codable {
    var name: String
    var age: Int
    var street: String
    var state: String

    private enum CodingKeys: String, CodingKey {
        case name
        case age
        case street = "Street_name"
        case state
    } }
Renjish C
fonte