Wie verwende ich benutzerdefinierte Schlüssel mit dem Decodable-Protokoll von Swift 4?

102

Swift 4 führte die Unterstützung für die native JSON-Codierung und -Decodierung über das DecodableProtokoll ein. Wie verwende ich dafür benutzerdefinierte Schlüssel?

Angenommen, ich habe eine Struktur

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

Ich kann dies in JSON codieren.

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

Ich kann dies wieder in ein Objekt kodieren.

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

Aber wenn ich ein JSON-Objekt hätte, wäre das

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

Wie würde ich dem Decoder auf Addressdiesen zip_codeKarten sagen zip? Ich glaube, Sie verwenden das neue CodingKeyProtokoll, aber ich kann nicht herausfinden, wie ich es verwenden soll.

chrismanderson
quelle

Antworten:

257

Manuelles Anpassen der Codierungsschlüssel

In Ihrem Beispiel erhalten Sie eine automatisch generierte Konformität mit, Codableda auch alle Ihre Eigenschaften übereinstimmen Codable. Diese Konformität erstellt automatisch einen Schlüsseltyp, der einfach den Eigenschaftsnamen entspricht. Dieser wird dann verwendet, um aus einem einzelnen Schlüsselcontainer zu codieren / daraus zu decodieren.

Doch ein wirklich nettes Feature dieser automatisch generierten Übereinstimmung besteht darin , dass , wenn Sie definieren eine verschachtelte enumin Ihrem Typ „bezeichnet CodingKeys“ (oder eine Verwendung typealiasmit diesem Namen) , dass entspricht die CodingKeyProtokoll - Swift wird automatisch verwenden diese als Schlüsseltyp. Auf diese Weise können Sie die Schlüssel, mit denen Ihre Eigenschaften codiert / decodiert werden, einfach anpassen.

Das heißt also, Sie können einfach sagen:

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

Die Enum-Fallnamen müssen mit den Eigenschaftsnamen übereinstimmen, und die Rohwerte dieser Fälle müssen mit den Schlüsseln übereinstimmen, in die Sie codieren / von denen Sie decodieren (sofern nicht anders angegeben, entsprechen die Rohwerte einer StringAufzählung den Fallnamen ). Daher wird die zipEigenschaft jetzt mit dem Schlüssel codiert / decodiert "zip_code".

Die genauen Regeln für die automatische Generierung Encodable/ DecodableKonformität sind im Evolutionsvorschlag (Schwerpunkt Mine) aufgeführt:

Zusätzlich zur automatischen CodingKeyAnforderungssynthese für enumskönnen Encodable& DecodableAnforderungen auch für bestimmte Typen automatisch synthetisiert werden:

  1. Typen, Encodablederen Eigenschaften alle entsprechen, Encodableerhalten automatisch generierte Enum-Mapping-Eigenschaften für StringFallnamen CodingKey. Ähnliches gilt für DecodableTypen, deren Eigenschaften alle sindDecodable

  2. Typen, die in (1) fallen - und Typen, die manuell einen CodingKey enum(benannten CodingKeys, direkten oder über a typealias) bereitstellen , dessen Fälle 1-zu-1 Encodable/ DecodableEigenschaften nach Namen zuordnen - erhalten eine automatische Synthese von init(from:)und encode(to:)gegebenenfalls unter Verwendung dieser Eigenschaften und Schlüssel

  3. Typen , die in keiner fallen (1) noch (2) wird eine benutzerdefinierte Schlüsseltyp zur Verfügung stellen müssen , falls erforderlich und stellen ihre eigenen init(from:)und encode(to:), gegebenenfalls

Beispielcodierung:

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

Beispieldecodierung:

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

Automatische snake_caseJSON-Schlüssel für camelCaseEigenschaftsnamen

Wenn Sie in Swift 4.1 Ihre zipEigenschaft in umbenennen zipCode, können Sie die Schlüsselcodierungs- / Decodierungsstrategien für JSONEncoderund nutzen JSONDecoder, um Codierungsschlüssel zwischen camelCaseund automatisch zu konvertieren snake_case.

Beispielcodierung:

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

Beispieldecodierung:

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

Eine wichtige Anmerkung zu dieser Strategie ist jedoch, dass einige Eigenschaftsnamen nicht mit Akronymen oder Initialismen umgangen werden können, die gemäß den Swift API-Entwurfsrichtlinien (je nach Position) einheitlich in Groß- oder Kleinschreibung geschrieben werden sollten ).

Beispielsweise wird eine Eigenschaft mit dem Namen someURLmit dem Schlüssel codiert some_url, aber beim Decodieren wird diese in transformiert someUrl.

Um dies zu beheben, müssen Sie den Codierungsschlüssel für diese Eigenschaft manuell als Zeichenfolge angeben, die der Decoder erwartet, z. B. someUrlin diesem Fall (der some_urlvom Encoder weiterhin transformiert wird):

struct S : Codable {

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

  var someURL: String
  var someOtherProperty: String
}

(Dies beantwortet Ihre spezifische Frage nicht unbedingt, aber angesichts des kanonischen Charakters dieser Fragen und Antworten halte ich es für sinnvoll, sie einzubeziehen.)

Benutzerdefinierte automatische JSON-Schlüsselzuordnung

In Swift 4.1 können Sie die benutzerdefinierten Schlüsselcodierungs- / Decodierungsstrategien für JSONEncoderund nutzen JSONDecoderund eine benutzerdefinierte Funktion zum Zuordnen von Codierungsschlüsseln bereitstellen.

Die von Ihnen bereitgestellte Funktion verwendet a [CodingKey], das den Codierungspfad für den aktuellen Punkt beim Codieren / Decodieren darstellt (in den meisten Fällen müssen Sie nur das letzte Element berücksichtigen, dh den aktuellen Schlüssel). Die Funktion gibt a zurück CodingKey, das den letzten Schlüssel in diesem Array ersetzt.

Beispiel: UpperCamelCaseJSON-Schlüssel für lowerCamelCaseEigenschaftsnamen:

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

Sie können jetzt mit der .convertToUpperCamelCaseSchlüsselstrategie codieren :

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

und mit der .convertFromUpperCamelCaseSchlüsselstrategie dekodieren :

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
quelle
Bin selbst darauf gestoßen! Ich frage mich, gibt es eine Möglichkeit, nur den einen Schlüssel zu überschreiben, den ich ändern möchte, und den Rest in Ruhe zu lassen? ZB in der case-Anweisung unter der CodingKeysAufzählung; Kann ich nur den einen Schlüssel auflisten, den ich ändere?
Chrismanderson
2
"""ist für ein mehrzeiliges Literal :)
Martin R
6
@ MartinR Oder auch nur ein einziges Zeilenliteral, ohne entkommen zu müssen "s: D
Hamish
1
@chrismanderson Genau - insbesondere angesichts der Tatsache, dass der Compiler erzwingt, dass die Fallnamen mit den Eigenschaftsnamen synchron gehalten werden (es wird eine Fehlermeldung angezeigt, dass Sie sich nicht an etwas Codableanderes halten)
Hamish
1
@ClayEllis Ah ja, obwohl die Verwendung der verschachtelten Container zum Beispiel direkt im Initialisierer natürlich Addressunnötig dazu führt, dass ein JSON-Objekt dekodiert wird, das an einer bestimmten Stelle im übergeordneten Objektdiagramm beginnt. Es wäre viel schöner, den Startschlüsselpfad bis zum Decoder selbst zu abstrahieren - hier ist eine grobe hackey-artige Implementierung .
Hamish
17

Mit Swift 4.2 können Sie je nach Bedarf eine der drei folgenden Strategien verwenden, damit die benutzerdefinierten Eigenschaftsnamen Ihrer Modellobjekte mit Ihren JSON-Schlüsseln übereinstimmen.


# 1. Verwenden von benutzerdefinierten Codierungsschlüsseln

Wenn Sie eine Struktur deklarieren, die der folgenden Implementierung entspricht Codable( Decodableund diese Encodableprotokolliert) ...

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

... generiert der Compiler automatisch eine verschachtelte Aufzählung, die dem CodingKeyProtokoll für Sie entspricht.

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

Wenn die in Ihrem serialisierten Datenformat verwendeten Schlüssel nicht mit den Eigenschaftsnamen Ihres Datentyps übereinstimmen, können Sie diese Aufzählung manuell implementieren und die rawValuefür die erforderlichen Fälle geeignete festlegen .

Das folgende Beispiel zeigt, wie es geht:

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 (Ersetzen der zipEigenschaft durch den JSON-Schlüssel "Postleitzahl"):

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

Dekodieren (JSON-Schlüssel "Postleitzahl" durch zipEigenschaft ersetzen ):

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. Verwenden von Snake Case zum Kamelfall Schlüsselcodierungsstrategien

Wenn Ihr JSON schlangen verrohrten Schlüssel hat und Sie wollen , dass sie Kamel Gefasste Eigenschaften für Ihr Modell Objekt konvertieren, können Sie Ihre Set JSONEncoder‚s keyEncodingStrategyund JSONDecoder‘ s keyDecodingStrategyEigenschaften .convertToSnakeCase.

Das folgende Beispiel zeigt, wie es geht:

import Foundation

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

Encode (Konvertieren von Eigenschaften in Kamelhülle in JSON-Schlüssel mit Schlangenhülle):

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

Dekodieren (Konvertieren von JSON-Schlüsseln mit Schlangengehäuse in Eigenschaften mit Kamelgehäuse):

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. Verwenden benutzerdefinierter Schlüsselcodierungsstrategien

Falls erforderlich, JSONEncoderund JSONDecoderermöglicht es Ihnen , eine benutzerdefinierte Strategie setzen Kodierschlüssel zur Karte mit JSONEncoder.KeyEncodingStrategy.custom(_:)und JSONDecoder.KeyDecodingStrategy.custom(_:).

Das folgende Beispiel zeigt, wie sie implementiert werden:

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

Codieren (Konvertieren von Eigenschaften für Kleinbuchstaben in Großbuchstaben in JSON-Schlüssel für Großbuchstaben):

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

Dekodieren (Konvertieren von JSON-Schlüsseln für Großbuchstaben in Kleinbuchstaben in Kleinbuchstaben):

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

Quellen:

Imanou Petit
quelle
3

Was ich getan habe, ist eine eigene Struktur zu erstellen, genau wie Sie es von JSON in Bezug auf seine Datentypen erhalten.

Genau wie dieser:

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
}

Danach müssen Sie eine Erweiterung derselben structErweiterung decodableund enumderselben Struktur mit erstellen CodingKeyund dann den Decoder mithilfe dieser Aufzählung mit seinen Schlüsseln und Datentypen initialisieren (Schlüssel kommen aus der Aufzählung und die Datentypen kommen oder sagen aus der Struktur selbst referenziert)

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

Sie müssen hier jeden Schlüssel und jeden Datentyp entsprechend Ihren Anforderungen ändern und mit dem Decoder verwenden.

Tushar
quelle
-1

Mit CodingKey können Sie benutzerdefinierte Schlüssel in codierbaren oder decodierbaren Protokollen verwenden.

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.
quelle