¿Cómo utilizo teclas personalizadas con el protocolo decodificable de Swift 4?

102

Swift 4 introdujo soporte para la codificación y decodificación JSON nativa a través del Decodableprotocolo. ¿Cómo utilizo claves personalizadas para esto?

Por ejemplo, digamos que tengo una estructura

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

Puedo codificar esto en 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" 
           }
    }
}

Puedo codificar esto de nuevo a un objeto.

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

Pero si tuviera un objeto json que fuera

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

¿Cómo le diría al decodificador en Addressesos zip_codemapas zip? Creo que usas el nuevo CodingKeyprotocolo, pero no sé cómo usarlo.

Chrismanderson
fuente

Respuestas:

258

Personalizar manualmente las claves de codificación

En su ejemplo, obtiene una conformidad generada automáticamente, Codableya que todas sus propiedades también cumplen Codable. Esta conformidad crea automáticamente un tipo de clave que simplemente se corresponde con los nombres de las propiedades, que luego se usa para codificar / decodificar desde un único contenedor con clave.

Sin embargo, una muy buena característica de este conformidad autogenerado es que si se define un anidado enumen su tipo llamado " CodingKeys" (o usar una typealiascon este nombre) que se ajusta al CodingKeyprotocolo - Swift utilizarán automáticamente este como el tipo de clave. Por lo tanto, esto le permite personalizar fácilmente las claves con las que se codifican / decodifican sus propiedades.

Entonces, lo que esto significa es que puedes decir:

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

Los nombres de los casos de enumeración deben coincidir con los nombres de las propiedades, y los valores sin procesar de estos casos deben coincidir con las claves que está codificando / decodificando (a menos que se especifique lo contrario, los valores sin procesar de una Stringenumeración serán los mismos que los nombres de los casos ). Por lo tanto, la zippropiedad ahora se codificará / decodificará con la clave "zip_code".

Las reglas exactas para la conformidad Encodable/ autogenerado Decodablese detallan en la propuesta de evolución (el énfasis es mío):

Además de la CodingKeysíntesis automática de requisitos para enums, Encodable& los Decodablerequisitos también se pueden sintetizar automáticamente para ciertos tipos:

  1. Los tipos que se ajustan a Encodablecuyas propiedades son todas Encodableobtienen un mapeo de propiedades de enumeración Stringrespaldado generado automáticamente CodingKeya los nombres de caso. Similarmente para Decodabletipos cuyas propiedades son todasDecodable

  2. Los tipos que caen en (1) - y los tipos que proporcionan manualmente un CodingKey enum(con nombre CodingKeys, directamente o mediante a typealias) cuyos casos se asignan 1 a 1 a Encodable/ Decodablepropiedades por nombre - obtienen una síntesis automática de init(from:)y, encode(to:)según corresponda, utilizando esas propiedades y claves

  3. Los tipos que no pertenecen ni a (1) ni a (2) tendrán que proporcionar un tipo de clave personalizada si es necesario y proporcionar la suya propia init(from:)y encode(to:), según corresponda

Codificación de ejemplo:

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

Ejemplo de decodificación:

// 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_caseClaves JSON automáticas para camelCasenombres de propiedades

En Swift 4.1, si cambia el nombre de su zippropiedad a zipCode, puede aprovechar las estrategias de codificación / decodificación de claves en JSONEncodery JSONDecoderpara convertir automáticamente las claves de codificación entre camelCasey snake_case.

Codificación de ejemplo:

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

Ejemplo de decodificación:

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

Sin embargo, una cosa importante a tener en cuenta acerca de esta estrategia es que no podrá intercambiar algunos nombres de propiedades con acrónimos o iniciales que, de acuerdo con las pautas de diseño de la API de Swift , deben ser uniformemente en mayúsculas o minúsculas (dependiendo de la posición). ).

Por ejemplo, una propiedad nombrada someURLse codificará con la clave some_url, pero al decodificar, se transformará en someUrl.

Para solucionar esto, tendrá que especificar manualmente la clave de codificación para que esa propiedad sea la cadena que espera el decodificador, por ejemplo, someUrlen este caso (que aún será transformado some_urlpor el codificador):

struct S : Codable {

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

  var someURL: String
  var someOtherProperty: String
}

(Esto no responde estrictamente a su pregunta específica, pero dada la naturaleza canónica de esta sesión de preguntas y respuestas, creo que vale la pena incluirla)

Asignación automática de claves JSON personalizada

En Swift 4.1, puede aprovechar las estrategias de codificación / decodificación de claves personalizadas en JSONEncodery JSONDecoder, lo que le permite proporcionar una función personalizada para asignar claves de codificación.

La función que proporcionas toma a [CodingKey], que representa la ruta de codificación para el punto actual en la codificación / decodificación (en la mayoría de los casos, solo necesitarás considerar el último elemento, es decir, la clave actual). La función devuelve un CodingKeyque reemplazará la última clave de esta matriz.

Por ejemplo, UpperCamelCaseclaves JSON para lowerCamelCasenombres de propiedad:

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

Ahora puede codificar con la .convertToUpperCamelCaseestrategia clave:

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

y decodificar con la .convertFromUpperCamelCaseestrategia clave:

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
fuente
¡Me encontré con esto yo mismo! Me pregunto, ¿hay alguna manera de anular solo la tecla que quiero cambiar y dejar el resto solo? Por ejemplo, en la declaración de caso, bajo la CodingKeysenumeración; ¿Puedo enumerar la clave que estoy cambiando?
chrismanderson
2
"""es para un literal de varias líneas :)
Martin R
6
@MartinR O incluso una sola línea literal sin tener que escapar "s: D
Hamish
1
@chrismanderson Exactamente, especialmente dado que el compilador exige que los nombres de los casos se mantengan sincronizados con los nombres de las propiedades (le dará un error que le dirá que no cumple con lo Codablecontrario)
Hamish
1
@ClayEllis Ah, sí, aunque, por supuesto, usar los contenedores anidados, por ejemplo, directamente en el inicializador de se Addressvincula innecesariamente a la decodificación de un objeto JSON que comienza en un lugar específico en el gráfico del objeto principal. Sería mucho más agradable abstraer la ruta de la clave de inicio hasta el decodificador mismo: aquí hay una implementación aproximada de hackey .
Hamish
17

Con Swift 4.2, de acuerdo con sus necesidades, puede utilizar una de las 3 estrategias siguientes para hacer que los nombres de propiedad personalizados de los objetos de su modelo coincidan con sus claves JSON.


# 1. Usar claves de codificación personalizadas

Cuando declara una estructura que se ajusta a Codable( Decodabley Encodableprotocolos) con la siguiente implementación ...

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

... el compilador genera automáticamente una enumeración anidada que se ajusta al CodingKeyprotocolo para usted.

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

Por lo tanto, si las claves utilizadas en su formato de datos serializados no coinciden con los nombres de propiedad de su tipo de datos, puede implementar manualmente esta enumeración y establecer la apropiada rawValuepara los casos requeridos.

El siguiente ejemplo muestra cómo hacerlo:

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 (reemplazando la zippropiedad con la clave 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 (reemplazando la clave JSON "zip_code" con la zippropiedad):

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. Uso de estrategias de codificación clave de caso de serpiente a caso de camello

Si su JSON tiene teclas de serpiente entubado y desea convertirlos a propiedades de camellos con carcasa para su modelo de objetos, puede configurar su JSONEncoder's keyEncodingStrategyy JSONDecoder' s keyDecodingStrategypropiedades a .convertToSnakeCase.

El siguiente ejemplo muestra cómo hacerlo:

import Foundation

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

Codificar (convertir propiedades en caja de camello en claves JSON en caja de serpiente):

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 (convertir claves JSON en caja de serpiente en propiedades en caja de camello):

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. Uso de estrategias de codificación de claves personalizadas

Si es necesario, JSONEncodery le JSONDecoderpermite establecer una estrategia personalizada para asignar claves de codificación usando JSONEncoder.KeyEncodingStrategy.custom(_:)y JSONDecoder.KeyDecodingStrategy.custom(_:).

El siguiente ejemplo muestra cómo implementarlos:

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 (convertir las propiedades de la primera letra en minúsculas en claves JSON de la primera letra en mayú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"}
 */

Decodificación (conversión de claves JSON de la primera letra en mayúscula en propiedades de la primera letra en 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")
 */

Fuentes:

Imanou Petit
fuente
3

Lo que he hecho es crear una estructura propia como la que obtiene del JSON con respecto a sus tipos de datos.

Así:

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
}

Después de esto, debe crear una extensión de la misma structextensión decodabley enumde la misma estructura con CodingKeyy luego debe inicializar el decodificador usando esta enumeración con sus claves y tipos de datos (las claves vendrán de la enumeración y los tipos de datos vendrán o dirán referenciado desde la propia estructura)

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

Debe cambiar aquí todas y cada una de las claves y tipos de datos de acuerdo con sus necesidades y usarlas con el decodificador.

Tushar
fuente
-1

Al usar CodingKey , puede usar claves personalizadas en un protocolo codificable o decodificable.

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
fuente