Come si usano i tasti personalizzati con il protocollo Decodable di Swift 4?


102

Swift 4 ha introdotto il supporto per la codifica e decodifica JSON nativa tramite il Decodableprotocollo. Come si usano le chiavi personalizzate per questo?

Ad esempio, diciamo che ho una struttura

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

Posso codificarlo in 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 codificare questo di nuovo in un oggetto.

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

Ma se avessi un oggetto json, quello era

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

Come potrei dire al decoder su Addressquelle zip_codemappe di zip? Credo che tu usi il nuovo CodingKeyprotocollo, ma non riesco a capire come usarlo.

Risposte:


258

Personalizzazione manuale delle chiavi di codifica

Nel tuo esempio, stai ottenendo una conformità generata automaticamente a Codablecome sono conformi anche tutte le tue proprietà Codable. Questa conformità crea automaticamente un tipo di chiave che corrisponde semplicemente ai nomi delle proprietà, che viene quindi utilizzato per codificare / decodificare da un singolo contenitore con chiave.

Tuttavia uno davvero valida caratteristica di questa conformità generato automaticamente è che se si definisce un annidata enumnel vostro tipo chiamato " CodingKeys" (o utilizzare un typealiascon questo nome) conforme al CodingKeyprotocollo - Swift utilizzeranno automaticamente questo come il tipo di chiave. Ciò consente quindi di personalizzare facilmente le chiavi con cui vengono codificate / decodificate le proprietà.

Quindi questo significa che puoi semplicemente dire:

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

I nomi dei casi enum devono corrispondere ai nomi delle proprietà e i valori grezzi di questi casi devono corrispondere alle chiavi da cui stai codificando / decodificando (se non diversamente specificato, i valori grezzi di Stringun'enumerazione saranno gli stessi dei nomi dei casi ). Pertanto, la zipproprietà verrà ora codificata / decodificata utilizzando la chiave "zip_code".

Le regole esatte per l'auto-generazione Encodable/ Decodableconformità sono dettagliate dalla proposta di evoluzione (enfasi mia):

Oltre ad automatico CodingKeysintesi requisito enums, Encodablee Decodablerequisiti possono essere sintetizzati automaticamente per determinati tipi come bene:

  1. Tipi conformi alla Encodablecui proprietà sono tutti Encodableottenere un generate automaticamente Stringemessi a fronte di CodingKeyproprietà di mappatura enum ai nomi di casi. Allo stesso modo per i Decodabletipi le cui proprietà sono tutteDecodable

  2. I tipi che rientrano in (1) - e i tipi che forniscono manualmente a CodingKey enum(denominato CodingKeys, direttamente o tramite a typealias) i cui casi mappano 1-a-1 a Encodable/ Decodableproprietà per nome - ottengono la sintesi automatica init(from:)e, encode(to:)se appropriato, utilizzando tali proprietà e chiavi

  3. I tipi che non rientrano né in (1) né in (2) dovranno fornire un tipo di chiave personalizzato se necessario e fornire il proprio init(from:)e encode(to:), a seconda dei casi

Codifica di esempio:

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

Esempio di decodifica:

// 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_caseChiavi JSON automatiche per camelCasei nomi delle proprietà

In Swift 4.1, se rinomini la tua zipproprietà in zipCode, puoi sfruttare le strategie di codifica / decodifica delle chiavi su JSONEncodere JSONDecoderper convertire automaticamente le chiavi di codifica tra camelCasee snake_case.

Codifica di esempio:

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

Esempio di decodifica:

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

Una cosa importante da notare su questa strategia, tuttavia, è che non sarà in grado di eseguire il round trip di alcuni nomi di proprietà con acronimi o inizializzazioni che, secondo le linee guida di progettazione dell'API Swift , dovrebbero essere uniformemente maiuscole o minuscole (a seconda della posizione ).

Ad esempio, una proprietà denominata someURLverrà codificata con la chiave some_url, ma durante la decodifica, questa verrà trasformata in someUrl.

Per risolvere questo problema, dovrai specificare manualmente la chiave di codifica per quella proprietà come una stringa che il decoder si aspetta, ad esempio someUrlin questo caso (che verrà comunque trasformata some_urldal codificatore):

struct S : Codable {

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

  var someURL: String
  var someOtherProperty: String
}

(Questo non risponde rigorosamente alla tua domanda specifica, ma data la natura canonica di questa domanda e risposta, credo che valga la pena includerla)

Mappatura delle chiavi JSON automatica personalizzata

In Swift 4.1, puoi sfruttare le strategie di codifica / decodifica delle chiavi personalizzate su JSONEncodere JSONDecoder, che ti consentono di fornire una funzione personalizzata per mappare le chiavi di codifica.

La funzione che fornisci accetta a [CodingKey], che rappresenta il percorso di codifica per il punto corrente nella codifica / decodifica (nella maggior parte dei casi, dovrai solo considerare l'ultimo elemento, cioè la chiave corrente). La funzione restituisce un CodingKeyche sostituirà l'ultima chiave in questo array.

Ad esempio, UpperCamelCasechiavi JSON per lowerCamelCasenomi di proprietà:

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

Ora puoi codificare con la .convertToUpperCamelCasestrategia chiave:

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 decodifica con la .convertFromUpperCamelCasestrategia chiave:

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

Sono appena incappato in questo da solo! Mi chiedo, c'è un modo per sovrascrivere solo l'unica chiave che voglio cambiare e lasciare il resto da solo? Ad esempio, nell'istruzione case, sotto l' CodingKeysenum; posso solo elencare l'unica chiave che sto cambiando?
chrismanderson

2
"""è per un letterale multilinea :)
Martin R

6
@MartinR O anche solo una singola riga letterale senza dover scappare "s: D
Hamish

1
@chrismanderson Esattamente - soprattutto dato che il compilatore impone che i nomi dei casi siano mantenuti sincronizzati con i nomi delle proprietà (ti darà un errore che ti dice che non sei conforme al Codablecontrario)
Hamish

1
@ClayEllis Ah sì, anche se ovviamente usare i contenitori annidati, ad esempio, direttamente nell'inizializzatore di Addressinutilmente, ti lega alla decodifica di un oggetto JSON che inizia in un punto specifico nell'oggetto grafico genitore. Sarebbe molto più bello astrarre il percorso della chiave di partenza fino al decodificatore stesso: ecco un'implementazione approssimativa .
Hamish

17

Con Swift 4.2, in base alle tue esigenze, puoi utilizzare una delle 3 strategie seguenti per fare in modo che i nomi delle proprietà personalizzate degli oggetti del modello corrispondano alle tue chiavi JSON.


# 1. Utilizzo di chiavi di codifica personalizzate

Quando dichiari una struttura conforme a Codable( Decodablee ai Encodableprotocolli) con la seguente implementazione ...

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

... il compilatore genera automaticamente un'enumerazione annidata conforme al CodingKeyprotocollo per te.

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

Pertanto, se le chiavi utilizzate nel formato dei dati serializzati non corrispondono ai nomi delle proprietà del tipo di dati, è possibile implementare manualmente questa enumerazione e impostare quella appropriata rawValueper i casi richiesti.

L'esempio seguente mostra come fare:

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

Codifica (sostituendo la zipproprietà con la chiave 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"}
 */

Decodifica (sostituendo la chiave JSON "zip_code" con zipproprietà):

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. Utilizzo delle strategie di codifica della chiave della custodia del serpente per la custodia del cammello

Se il tuo JSON ha chiavi con involucro di serpente e desideri convertirle in proprietà con involucro di cammello per il tuo oggetto modello, puoi impostare le proprietà JSONEncoder" keyEncodingStrategye JSONDecoder" keyDecodingStrategysu .convertToSnakeCase.

L'esempio seguente mostra come fare:

import Foundation

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

Codifica (conversione delle proprietà con involucro di cammello in chiavi JSON con involucro di serpente):

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

Decodifica (conversione di chiavi JSON con involucro di serpente in proprietà con involucro di cammello):

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. Utilizzo di strategie di codifica delle chiavi personalizzate

Se necessario, JSONEncodere JSONDecoderconsentono di impostare una strategia personalizzata per mappare le chiavi di codifica utilizzando JSONEncoder.KeyEncodingStrategy.custom(_:)e JSONDecoder.KeyDecodingStrategy.custom(_:).

L'esempio seguente mostra come implementarli:

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

Codifica (conversione delle proprietà della prima lettera minuscola in chiavi JSON della prima lettera maiuscola):

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

Decodifica (conversione delle chiavi JSON della prima lettera maiuscola in proprietà della prima lettera minuscola):

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

Fonti:


3

Quello che ho fatto è creare una propria struttura proprio come quello che stai ottenendo da JSON rispetto ai suoi tipi di dati.

Proprio come questo:

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
}

Dopodiché è necessario creare un'estensione della stessa structestensione decodablee enumdella stessa struttura con CodingKeye quindi è necessario inizializzare il decodificatore usando questo enum con le sue chiavi e tipi di dati (le chiavi verranno dall'enumerazione e i tipi di dati arriveranno o diranno referenziato dalla struttura stessa)

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

È necessario modificare qui ogni chiave e tipo di dati in base alle proprie esigenze e utilizzarlo con il decoder.


-1

Utilizzando CodingKey è possibile utilizzare chiavi personalizzate nel protocollo codificabile o decodificabile.

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
    } }
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.