Comment utiliser des clés personnalisées avec le protocole décodable de Swift 4?

102

Swift 4 introduit support pour l' encodage et le décodage JSON native par le biais du Decodableprotocole. Comment utiliser des clés personnalisées pour cela?

Par exemple, disons que j'ai une structure

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

Je peux encoder cela 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" 
           }
    }
}

Je peux encoder cela en un objet.

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

Mais si j'avais un objet json qui était

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

Comment pourrais-je indiquer au décodeur sur Addresscette zip_codecarte zip? Je pense que vous utilisez le nouveau CodingKeyprotocole, mais je n'arrive pas à comprendre comment l'utiliser.

chrismanderson
la source

Réponses:

258

Personnalisation manuelle des clés de codage

Dans votre exemple, vous obtenez une conformité générée automatiquement à Codablecar toutes vos propriétés sont également conformes à Codable. Cette conformité crée automatiquement un type de clé qui correspond simplement aux noms de propriété - qui est ensuite utilisé pour encoder / décoder à partir d'un seul conteneur à clé.

Cependant , une très caractéristique propre de cette conformité est généré automatiquement que si vous définissez un imbriqué enumdans votre type appelé « CodingKeys» (ou utiliser un typealiasavec ce nom) qui est conforme au CodingKeyprotocole - Swift utilisera automatiquement ce que le type de clé. Cela vous permet donc de personnaliser facilement les clés avec lesquelles vos propriétés sont encodées / décodées.

Donc, ce que cela signifie, c'est que vous pouvez simplement 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
    }
}

Les noms de cas d'énumération doivent correspondre aux noms de propriété, et les valeurs brutes de ces cas doivent correspondre aux clés avec lesquelles vous encodez / décodez (sauf indication contraire, les valeurs brutes d'une Stringénumération seront les mêmes que les noms de cas ). Par conséquent, la zippropriété sera désormais encodée / décodée à l'aide de la clé "zip_code".

Les règles exactes de l'auto-généré Encodable/ Decodableconformité sont détaillées par la proposition d'évolution (c'est moi qui souligne):

En plus de la CodingKeysynthèse automatique des exigences pour enums, Encodable& les Decodableexigences peuvent être automatiquement synthétisées pour certains types:

  1. Les types conformes Encodabledont les propriétés sont toutes Encodableobtiennent une énumération enum générée automatiquement Stringqui CodingKeymappe les propriétés aux noms de cas. De même pour les Decodabletypes dont les propriétés sont toutesDecodable

  2. Les types tombant dans (1) - et les types qui fournissent manuellement un CodingKey enum(named CodingKeys, directement ou via a typealias) dont les cas mappent 1-to-1 à Encodable/ Decodableproperties by name - obtiennent une synthèse automatique de init(from:)et, encode(to:)le cas échéant, en utilisant ces propriétés et clés

  3. Les types qui ne relèvent ni de (1) ni (2) devront fournir un type de clé personnalisé si nécessaire et fournir le leur init(from:)et encode(to:), le cas échéant

Exemple d'encodage:

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

Exemple de décodage:

// 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_caseClés JSON automatiques pour camelCaseles noms de propriétés

Dans Swift 4.1, si vous renommez votre zippropriété en zipCode, vous pouvez profiter des stratégies d'encodage / décodage de clé sur JSONEncoderet JSONDecoderafin de convertir automatiquement les clés de codage entre camelCaseet snake_case.

Exemple d'encodage:

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

Exemple de décodage:

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

Cependant, une chose importante à noter à propos de cette stratégie est qu'elle ne pourra pas aller-retour pour certains noms de propriétés avec des acronymes ou des initialismes qui, selon les directives de conception de l'API Swift , doivent être uniformément en majuscules ou en minuscules (selon la position ).

Par exemple, une propriété nommée someURLsera encodée avec la clé some_url, mais lors du décodage, elle sera transformée en someUrl.

Pour résoudre ce problème, vous devrez spécifier manuellement la clé de codage pour cette propriété comme étant la chaîne attendue par le décodeur, par exemple someUrldans ce cas (qui sera toujours transformée some_urlpar l'encodeur):

struct S : Codable {

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

  var someURL: String
  var someOtherProperty: String
}

(Cela ne répond pas strictement à votre question spécifique, mais étant donné la nature canonique de ce Q&A, je pense que cela vaut la peine d'être inclus)

Mappage de clé JSON automatique personnalisé

Dans Swift 4.1, vous pouvez tirer parti des stratégies d'encodage / décodage de touches personnalisées sur JSONEncoderet JSONDecoder, vous permettant de fournir une fonction personnalisée pour mapper les clés de codage.

La fonction que vous fournissez prend a [CodingKey], qui représente le chemin de codage pour le point actuel dans le codage / décodage (dans la plupart des cas, vous n'aurez besoin de considérer que le dernier élément; c'est-à-dire la clé actuelle). La fonction renvoie un CodingKeyqui remplacera la dernière clé de ce tableau.

Par exemple, UpperCamelCaseles clés JSON pour lowerCamelCaseles noms de propriété:

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

Vous pouvez maintenant encoder avec la .convertToUpperCamelCasestratégie clé:

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

et décoder avec la .convertFromUpperCamelCasestratégie clé:

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
la source
Je suis juste tombé dessus moi-même! Je me demande, y a-t-il un moyen de ne remplacer que la clé que je veux changer et de laisser le reste seul? Par exemple, dans l'instruction case, sous l' CodingKeysénumération; puis-je simplement lister la clé que je change?
chrismanderson
2
"""est pour un littéral multiligne :)
Martin R
6
@MartinR Ou même juste une seule ligne littérale sans avoir à échapper "s: D
Hamish
1
@chrismanderson Exactement - d'autant plus que le compilateur fait en sorte que les noms de cas soient synchronisés avec les noms de propriété (cela vous donnera une erreur indiquant que vous ne vous conformez pas au Codablecontraire)
Hamish
1
@ClayEllis Ah oui, bien que bien sûr, l'utilisation des conteneurs imbriqués par exemple directement dans l'initialiseur de Addressvous lie inutilement au décodage d'un objet JSON qui commence à un endroit spécifique dans le graphe de l'objet parent. Il serait beaucoup plus agréable d'abstraire le chemin de la clé de départ jusqu'au décodeur lui-même - voici une implémentation approximative de hackey-ish .
Hamish
17

Avec Swift 4.2, selon vos besoins, vous pouvez utiliser l'une des 3 stratégies suivantes afin de faire correspondre les noms de propriétés personnalisées de vos objets modèles à vos clés JSON.


#1. Utilisation de clés de codage personnalisées

Lorsque vous déclarez une structure conforme à Codable( Decodableet aux Encodableprotocoles) avec l'implémentation suivante ...

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

... le compilateur génère automatiquement une énumération imbriquée conforme au CodingKeyprotocole pour vous.

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

Par conséquent, si les clés utilisées dans votre format de données sérialisées ne correspondent pas aux noms de propriété de votre type de données, vous pouvez implémenter manuellement cette énumération et définir la valeur appropriée rawValuepour les cas requis.

L'exemple suivant montre comment faire:

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

Encode (en remplaçant la zippropriété par la clé 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"}
 */

Décoder (en remplaçant la clé JSON "zip_code" par la zippropriété):

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. Utilisation des stratégies de codage de clé de cas de serpent à cas de chameau

Si votre JSON a des touches de serpent-tubé et que vous voulez les convertir en propriétés en CamelCase pour votre modèle objet, vous pouvez définir votre JSONEncoder« s keyEncodingStrategyet JSONDecoder» de keyDecodingStrategypropriétés à .convertToSnakeCase.

L'exemple suivant montre comment faire:

import Foundation

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

Encode (conversion des propriétés camel cased en clés JSON snake cased):

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

Decode (conversion des clés JSON casse snake en propriétés camel cased):

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. Utilisation de stratégies de codage de clé personnalisées

Si nécessaire, JSONEncoderet JSONDecodervous permet de définir une stratégie personnalisée pour mapper les clés de codage à l'aide de JSONEncoder.KeyEncodingStrategy.custom(_:)et JSONDecoder.KeyDecodingStrategy.custom(_:).

L'exemple suivant montre comment les implémenter:

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

Encode (conversion des propriétés de première lettre minuscules en clés JSON de première lettre majuscule):

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

Decode (conversion des clés JSON de première lettre majuscules en propriétés de première lettre minuscules):

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

Sources:

Imanou Petit
la source
3

Ce que j'ai fait, c'est créer ma propre structure, tout comme ce que vous obtenez du JSON en ce qui concerne ses types de données.

Juste comme ça:

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
}

Après cela, vous devez créer une extension de la même structextension decodableet enumde la même structure avec CodingKey, puis vous devez initialiser le décodeur en utilisant cette énumération avec ses clés et ses types de données (les clés proviendront de l'énumération et les types de données viendront ou diront référencé à partir de la structure elle-même)

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

Vous devez modifier ici chaque clé et chaque type de données en fonction de vos besoins et l'utiliser avec le décodeur.

Tushar
la source
-1

En utilisant CodingKey vous pouvez utiliser les touches personnalisées dans le protocole codable ou décodable.

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
la source