Jak używać kluczy niestandardowych z protokołem Decodable Swift 4?

102

Swift 4 wprowadził obsługę natywnego kodowania JSON i dekodowania za pośrednictwem Decodableprotokołu. Jak używać do tego kluczy niestandardowych?

Np. Powiedz, że mam strukturę

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

Mogę to zakodować do 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" 
           }
    }
}

Mogę to zakodować z powrotem do obiektu.

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

Ale gdybym miał obiekt json, to był

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

Jak mam powiedzieć dekoderowi na Addresstych zip_codemapach zip? Wydaje mi się, że używasz nowego CodingKeyprotokołu, ale nie mogę dowiedzieć się, jak go używać.

chrismanderson
źródło

Odpowiedzi:

258

Ręczne dostosowywanie kluczy kodowania

W swoim przykładzie otrzymujesz automatycznie wygenerowaną zgodność, z którą Codablesą zgodne również wszystkie twoje właściwości Codable. Ta zgodność automatycznie tworzy typ klucza, który po prostu odpowiada nazwom właściwości - który jest następnie używany do kodowania do / dekodowania z kontenera z pojedynczym kluczem.

Jednak jeden naprawdę fajną cechą tej automatycznie generowanej zgodności jest to, że jeśli zdefiniujesz zagnieżdżony enumw swoim typie o nazwie " CodingKeys" (lub użyjesz a typealiasz tą nazwą), który jest zgodny z CodingKeyprotokołem - Swift automatycznie użyje tego jako typu klucza. Dzięki temu możesz łatwo dostosować klucze, za pomocą których twoje właściwości są kodowane / dekodowane.

Oznacza to, że możesz po prostu powiedzieć:

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

Nazwy przypadków wyliczenia muszą pasować do nazw właściwości, a surowe wartości tych przypadków muszą być zgodne z kluczami, do których kodujesz / dekodujesz (o ile nie określono inaczej, surowe wartości Stringwyliczenia będą takie same jak nazwy przypadków ). Dlatego zipwłaściwość zostanie teraz zakodowana / zdekodowana przy użyciu klucza "zip_code".

Dokładne zasady generowania automatycznego Encodable / Decodablezgodności są szczegółowo opisane w propozycji ewolucji (moje podkreślenie):

Oprócz automatycznej CodingKeysyntezy wymagań dla enums , wymagania Encodable& Decodablemogą być również automatycznie syntetyzowane dla niektórych typów:

  1. Typy zgodne z Encodablewszystkimi właściwościami są Encodablegenerowane automatycznieString -backed CodingKeywłaściwości mapowania enum do nazw sprawy. Podobnie dla Decodabletypów, których wszystkie właściwości sąDecodable

  2. Typy należące do (1) - i typy, które ręcznie zapewniają CodingKey enum(nazwane CodingKeys, bezpośrednio lub przez a typealias), których przypadki mapują 1-do-1 na Encodable/Decodable właściwości według nazwy - uzyskują automatyczną syntezę init(from:)i, encode(to:)jeśli to konieczne, przy użyciu tych właściwości i kluczy

  3. Typy, które nie należą do ani (1), ani (2) będą musiały w razie potrzeby podać niestandardowy typ klucza i zapewnić własne init(from:)i encode(to:) , w razie potrzeby

Przykładowe kodowanie:

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

Przykładowe dekodowanie:

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

Automatyczny snake_case klucze JSON dla camelCasenazw właściwości

W Swift 4.1, jeśli zmienisz nazwę swojej zipwłaściwości na zipCode, możesz skorzystać ze strategii kodowania / dekodowania klucza na JSONEncoderiJSONDecoder w celu automatycznej konwersji kluczy kodowania między camelCasea snake_case.

Przykładowe kodowanie:

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

Przykładowe dekodowanie:

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

Jedną ważną rzeczą, na którą należy zwrócić uwagę na temat tej strategii, jest to, że nie będzie ona w stanie przelać w obie strony niektórych nazw właściwości za pomocą akronimów lub inicjałów, które zgodnie z wytycznymi projektowymi Swift API powinny być jednolicie duże lub małe (w zależności od pozycji ).

Na przykład właściwość o nazwie someURLzostanie zakodowana za pomocą klucza some_url, ale podczas dekodowania zostanie przekształcona na someUrl.

Aby to naprawić, musisz ręcznie określić klucz kodowania dla tej właściwości jako ciąg, którego dekoder oczekuje, np. someUrlW tym przypadku (który nadal będzie przekształcany some_urlprzez koder):

struct S : Codable {

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

  var someURL: String
  var someOtherProperty: String
}

(To nie jest dokładną odpowiedzią na twoje konkretne pytanie, ale biorąc pod uwagę kanoniczny charakter tych pytań i odpowiedzi, uważam, że warto je uwzględnić)

Niestandardowe automatyczne mapowanie kluczy JSON

W Swift 4.1 możesz skorzystać z niestandardowych strategii kodowania / dekodowania kluczy włączonych JSONEncoderi JSONDecoder, co pozwala na zapewnienie niestandardowej funkcji mapowania kluczy kodowania.

Podana funkcja przyjmuje znak a [CodingKey], który reprezentuje ścieżkę kodowania dla bieżącego punktu w kodowaniu / dekodowaniu (w większości przypadków wystarczy wziąć pod uwagę tylko ostatni element, czyli bieżący klucz). Funkcja zwraca wartość CodingKey, która zastąpi ostatni klucz w tej tablicy.

Na przykład UpperCamelCaseklucze JSON dla lowerCamelCasenazw właściwości:

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

Możesz teraz kodować za pomocą .convertToUpperCamelCasestrategii klucza:

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

i dekoduj z .convertFromUpperCamelCasekluczową strategią:

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
źródło
Właśnie się na to natknąłem! Zastanawiam się, czy istnieje sposób, aby zastąpić tylko jeden klucz, który chcę zmienić, a resztę zostawić w spokoju? Np. W przypadku instrukcji pod CodingKeyswyliczeniem; czy mogę wymienić tylko jeden klucz, który zmieniam?
chrismanderson
2
"""jest literałem wielowierszowym :)
Martin R
6
@MartinR Lub nawet pojedynczy wiersz bez ucieczki "s: D
Hamish
1
@chrismanderson Dokładnie - zwłaszcza biorąc pod uwagę, że kompilator wymusza, aby nazwy przypadków były zsynchronizowane z nazwami właściwości (spowoduje to błąd z informacją, że nie dostosowujesz się do Codableinnego)
Hamish
1
@ClayEllis Ach tak, chociaż oczywiście użycie zagnieżdżonych kontenerów na przykład bezpośrednio w inicjatorze Addressniepotrzebnie wiąże się z dekodowaniem obiektu JSON, który zaczyna się w określonym miejscu na grafie obiektu nadrzędnego. Byłoby znacznie przyjemniej wyodrębnić początkową ścieżkę klucza do samego dekodera - oto zgrubna implementacja hakerska .
Hamish
17

Dzięki Swift 4.2, zgodnie z własnymi potrzebami, możesz użyć jednej z 3 poniższych strategii, aby dopasować nazwy właściwości niestandardowych obiektów modelu do kluczy JSON.


# 1. Korzystanie z niestandardowych kluczy kodowania

Kiedy deklarujesz strukturę, która jest zgodna z Codable( Decodablei Encodableprotokołami) z następującą implementacją ...

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

... kompilator automatycznie generuje zagnieżdżone wyliczenie, które jest zgodne z CodingKeyprotokołem.

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

W związku z tym, jeśli klucze używane w serializowanym formacie danych nie są zgodne z nazwami właściwości z typu danych, można ręcznie zaimplementować to wyliczenie i ustawić odpowiednie rawValuedla wymaganych przypadków.

Poniższy przykład pokazuje, jak to zrobić:

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

Kodowanie (zastępując zipwłaściwość kluczem JSON „kod_zip”):

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

Dekodowanie (zastępowanie klucza JSON „kod_zip” zipwłaściwością):

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. Używanie etui węża do strategii kodowania kluczy w przypadku wielbłądów

Jeśli twój JSON ma klucze w kształcie węża i chcesz je przekonwertować na właściwości w obudowie wielbłąda dla obiektu modelu, możesz ustawić właściwości JSONEncoder's keyEncodingStrategyi JSONDecoder' s keyDecodingStrategyna .convertToSnakeCase.

Poniższy przykład pokazuje, jak to zrobić:

import Foundation

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

Kodowanie (konwersja właściwości w obudowie wielbłąda na klucze JSON w obudowie węża):

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

Dekodowanie (konwersja kluczy JSON w obudowie węża na właściwości w obudowie wielbłąda):

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. Korzystanie z niestandardowych strategii kodowania kluczy

W razie potrzeby JSONEncoderi JSONDecoderpozwala ustawić niestandardową strategię mapowania kluczy kodowania za pomocą JSONEncoder.KeyEncodingStrategy.custom(_:)i JSONDecoder.KeyDecodingStrategy.custom(_:).

Poniższy przykład pokazuje, jak je wdrożyć:

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

Kodowanie (konwertowanie właściwości pierwszej litery małej litery na klucze JSON pierwszej litery małej litery):

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

Dekodowanie (konwersja kluczy JSON z wielkimi literami do właściwości pierwszej litery małej litery):

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

Źródła:

Imanou Petit
źródło
3

To, co zrobiłem, to stworzenie własnej struktury, tak jak to, co otrzymujesz z JSON w odniesieniu do jego typów danych.

Takie jak to:

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
}

Następnie musisz utworzyć rozszerzenie tego samego structrozszerzenia decodablei tej enumsamej struktury z CodingKeya następnie musisz zainicjalizować dekoder używając tego wyliczenia z jego kluczami i typami danych (klucze będą pochodzić z wyliczenia i typy danych będą przychodzić lub mówić odwołuje się do samej struktury)

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

Musisz tutaj zmienić każdy klucz i typ danych zgodnie ze swoimi potrzebami i używać go z dekoderem.

Tushar
źródło
-1

Używając CodingKey , możesz używać niestandardowych kluczy w protokole kodowalnym lub dekodowalnym.

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
źródło