Como uso chaves personalizadas com o protocolo Decodificável do Swift 4?

O Swift 4 introduziu suporte para codificação e decodificação JSON nativas por meio do protocolo Decodable . 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 } 

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

Eu posso codificar isso de volta para um object.

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

Mas se eu tivesse um object json que fosse

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

Como eu diria ao decodificador no Address que zip_code mapeia para zip ? Eu acredito que você use o novo protocolo CodingKey , mas não consigo descobrir como usar isso.

    Personalizando manualmente as chaves de codificação

    No seu exemplo, você está recebendo uma conformidade automática com Codable pois todas as suas propriedades também estão de acordo com Codable . Essa conformidade cria automaticamente um tipo de chave que simplesmente corresponde aos nomes das propriedades – que são então usados ​​para codificar / decodificar a partir de um único contêiner com chave.

    No entanto, um recurso realmente interessante dessa conformidade gerada automaticamente é que, se você definir um enum nested em seu tipo chamado ” CodingKeys ” (ou usar um typealias com esse nome) que esteja em conformidade com o protocolo CodingKey , o Swift usará isso automaticamente como chave tipo. Isso permite que você personalize facilmente as chaves com as quais suas propriedades são codificadas / decodificadas.

    Então, o que 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 de 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 uma enumeração String serão os mesmos que os casos). nomes). Portanto, a propriedade zip agora será codificada / decodificada usando a chave "zip_code" .

    As regras exatas para a conformidade de Decodable / Encodable gerada Encodable são detalhadas pela proposta de evolução (ênfase minha):

    Além da síntese automática de requisitos de CodingKey para enums , os requisitos Decodable & Decodable podem ser automaticamente sintetizados para determinados tipos:

    1. Os tipos em conformidade com o Encodable cujas propriedades são todas Encodable obtêm propriedades de mapeamento de enum CodingKey String sequência geradas automaticamente para os nomes dos casos. Similarmente para os tipos Decodable cujas propriedades são todas Decodable

    2. Tipos que caem em (1) – e tipos que fornecem manualmente um enum CodingKey (chamado CodingKeys , diretamente, ou por meio de typealias ) cujos casos mapeiam 1-para-1 para propriedades Decodable / Decodable por nome – obtêm síntese automática de init(from:) e encode(to:) conforme apropriado, usando essas propriedades e chaves

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

    Exemplo de codificação:

     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_case JSON snake_case automáticas para nomes de propriedades camelCase

    No Swift 4.1 (disponível no Xcode 9.3 beta), se você renomear sua propriedade zip para zipCode , poderá aproveitar as principais estratégias de codificação / decodificação no JSONEncoder e no JSONDecoder para converter automaticamente as chaves de codificação entre camelCase e snake_case .

    Exemplo de codificação:

     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, porém, é que ela não será capaz de arredondar alguns nomes de propriedades com siglas ou inicialismos que, de acordo com as diretrizes de design do API Swift , devem ser uniformemente maiúsculos ou minúsculos (dependendo da posição ).

    Por exemplo, uma propriedade chamada someURL será codificada com a chave some_url , mas na decodificação, isso será transformado em someUrl .

    Para corrigir isso, você terá que especificar manualmente a chave de codificação para essa propriedade para ser string que o decodificador espera, por exemplo, someUrl , neste caso (que ainda será transformado em some_url pelo 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 dessa session de perguntas e respostas, acho que vale a pena include)

    Mapeamento de chave JSON automático personalizado

    No Swift 4.1 (disponível no Xcode 9.3 beta), você pode aproveitar as estratégias de codificação / decodificação de chave personalizadas no JSONEncoder e JSONDecoder , permitindo que você forneça uma function personalizada para mapear chaves de codificação.

    A function que você fornece recebe uma [CodingKey] , que representa o caminho de codificação para o ponto atual na codificação / decodificação (na maioria dos casos, você só precisará considerar o último elemento; ou seja, a chave atual). A function retorna uma CodingKey que replaceá a última chave dessa matriz.

    Por exemplo, UpperCamelCase chaves JSON do lowerCamelCase para nomes de propriedades lowerCamelCase :

     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 estratégia de chave .convertToUpperCamelCase :

     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 decodificar com a estratégia chave .convertFromUpperCamelCase :

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

    Quando você declara uma estrutura que está em conformidade com os Codable ( Decodable e Encodable ) com a seguinte implementação …

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

    … o compilador gera automaticamente um enum nested que está em conformidade com o protocolo CodingKey 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 das propriedades de seu tipo de dados, você precisará implementar manualmente esse enum e definir o rawValue apropriado para os casos necessários:

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

    Uso nº 1: codifique uma instância de Address em uma string JSON

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

    Uso # 2: decodificar uma string JSON em uma instância de Address

     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") */ 

    Fontes:

    • Codificação e decodificação de tipos personalizados
    • Sessão da WWDC 2017 212 “O que há de novo na Fundação”