Bagaimana cara menggunakan kunci khusus dengan protokol Decodable Swift 4?

102

Swift 4 memperkenalkan dukungan untuk encoding dan decoding JSON asli melalui Decodableprotokol. Bagaimana cara menggunakan kunci khusus untuk ini?

Misalnya, saya memiliki struct

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

Saya dapat menyandikan ini ke 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" 
           }
    }
}

Saya dapat menyandikannya kembali ke objek.

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

Tetapi jika saya memiliki objek json itu

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

Bagaimana saya akan memberitahu decoder pada Addressyang zip_codememetakan ke zip? Saya yakin Anda menggunakan CodingKeyprotokol baru , tetapi saya tidak tahu cara menggunakan ini.

chrismanderson
sumber

Jawaban:

258

Menyesuaikan kunci pengkodean secara manual

Dalam contoh Anda, Anda mendapatkan kesesuaian yang dibuat secara otomatis Codablekarena semua properti Anda juga mematuhi Codable. Kesesuaian ini secara otomatis membuat jenis kunci yang hanya sesuai dengan nama properti - yang kemudian digunakan untuk menyandikan ke / mendekode dari satu wadah kunci.

Namun satu fitur yang benar-benar rapi dari kesesuaian yang dihasilkan secara otomatis ini adalah jika Anda mendefinisikan sebuah bertingkat enumdalam tipe Anda yang disebut " CodingKeys" (atau menggunakan a typealiasdengan nama ini) yang sesuai dengan CodingKeyprotokol - Swift akan secara otomatis menggunakan ini sebagai tipe kunci. Oleh karena itu, hal ini memungkinkan Anda untuk dengan mudah menyesuaikan kunci yang properti Anda dienkode / didekodekan.

Artinya, Anda cukup mengatakan:

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

Nama kasus enum harus cocok dengan nama properti, dan nilai mentah kasus ini harus cocok dengan kunci yang Anda encoding / dekode dari (kecuali ditentukan lain, nilai mentah Stringenumerasi akan sama dengan nama kasus ). Oleh karena itu, zipproperti sekarang akan dienkode / didekodekan menggunakan kunci "zip_code".

Aturan yang tepat untuk otomatis Encodable/ Decodablekesesuaian dirinci oleh proposal evolusi (penekanan saya):

Selain CodingKeysintesis persyaratan otomatis untuk enums, Encodable& Decodablepersyaratan juga dapat disintesis secara otomatis untuk jenis tertentu:

  1. Jenis yang sesuai dengan Encodablepropertinya semuanya Encodablemendapatkan properti pemetaan enum yang Stringdidukung secara otomatis CodingKeyke nama kasus. Begitu pula untuk Decodabletipe yang propertinya semuanyaDecodable

  2. Jenis yang termasuk dalam (1) - dan jenis yang secara manual menyediakan CodingKey enum(dinamai CodingKeys, secara langsung, atau melalui a typealias) yang kasusnya memetakan 1-ke-1 ke Encodable/ Decodableproperti berdasarkan nama - dapatkan sintesis otomatis init(from:)dan encode(to:)jika sesuai, menggunakan properti dan kunci tersebut

  3. Jenis yang tidak termasuk dalam (1) maupun (2) harus menyediakan jenis kunci khusus jika diperlukan dan menyediakannya sendiri init(from:)dan encode(to:), sebagaimana mestinya

Contoh pengkodean:

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

Contoh decoding:

// 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_caseKunci JSON otomatis untuk camelCasenama properti

Di Swift 4.1, jika Anda mengganti nama zipproperti menjadi zipCode, Anda dapat memanfaatkan strategi encoding / decoding kunci pada JSONEncoderdan JSONDecoderuntuk secara otomatis mengonversi kunci pengkodean antara camelCasedan snake_case.

Contoh pengkodean:

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

Contoh decoding:

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

Namun, satu hal penting yang perlu diperhatikan tentang strategi ini adalah bahwa ia tidak akan dapat mengubah beberapa nama properti dengan akronim atau inisialisme yang, menurut pedoman desain API Swift , harus sama besar atau kecilnya (tergantung pada posisinya). ).

Misalnya, properti bernama someURLakan dienkode dengan kunci tersebut some_url, tetapi saat mendekode, ini akan diubah menjadi someUrl.

Untuk memperbaikinya, Anda harus secara manual menentukan kunci pengkodean untuk properti itu menjadi string yang diharapkan decoder, misalnya someUrldalam kasus ini (yang masih akan diubah some_urloleh encoder):

struct S : Codable {

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

  var someURL: String
  var someOtherProperty: String
}

(Ini tidak menjawab pertanyaan spesifik Anda secara ketat, tetapi mengingat sifat kanonik T&J ini, saya merasa ini layak untuk disertakan)

Pemetaan kunci JSON otomatis kustom

Di Swift 4.1, Anda dapat memanfaatkan strategi pengkodean / dekode kunci khusus JSONEncoderdan JSONDecoder, memungkinkan Anda menyediakan fungsi khusus untuk memetakan kunci pengkodean.

Fungsi yang Anda sediakan mengambil a [CodingKey], yang mewakili jalur pengkodean untuk titik saat ini dalam pengkodean / dekode (dalam banyak kasus, Anda hanya perlu mempertimbangkan elemen terakhir; yaitu, kunci saat ini). Fungsi mengembalikan a CodingKeyyang akan menggantikan kunci terakhir dalam larik ini.

Misalnya, UpperCamelCasekunci JSON untuk lowerCamelCasenama properti:

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

Anda sekarang dapat melakukan enkode dengan .convertToUpperCamelCasestrategi utama:

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

dan memecahkan kode dengan .convertFromUpperCamelCasestrategi kunci:

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
sumber
Baru saja tersandung pada ini sendiri! Saya bertanya-tanya, apakah ada cara untuk hanya mengganti satu kunci yang ingin saya ubah dan membiarkan yang lainnya? Misalnya dalam pernyataan kasus, di bawah CodingKeysenum; bisakah saya mencantumkan satu kunci yang saya ubah?
chrismanderson
2
"""adalah untuk literal multi-baris :)
Martin R
6
@MartinR Atau bahkan hanya satu baris literal tanpa harus melarikan diri "s: D
Hamish
1
@chrismanderson Persis - terutama mengingat bahwa kompilator memaksakan bahwa nama kasus tetap sinkron dengan nama properti (ini akan memberi Anda kesalahan yang mengatakan Anda tidak menyesuaikan diri dengan Codablesebaliknya)
Hamish
1
@ClayEllis Ah ya, meskipun tentu saja menggunakan wadah bersarang misalnya langsung di inisialisasi yang Addresstidak perlu mengikat diri Anda sendiri untuk mendekode objek JSON yang dimulai di tempat tertentu dalam grafik objek induk. Akan jauh lebih baik untuk mengabstraksi jalur kunci awal hingga dekoder itu sendiri - berikut adalah implementasi hackey-ish yang kasar .
Hamish
17

Dengan Swift 4.2, sesuai dengan kebutuhan Anda, Anda dapat menggunakan salah satu dari 3 strategi berikut untuk membuat nama properti khusus objek model Anda cocok dengan kunci JSON Anda.


# 1. Menggunakan kunci pengkodean khusus

Saat Anda mendeklarasikan struct yang sesuai dengan Codable( Decodabledan Encodableprotokol) dengan implementasi berikut ...

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

... kompilator secara otomatis menghasilkan enum bersarang yang sesuai dengan CodingKeyprotokol untuk Anda.

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

Oleh karena itu, jika kunci yang digunakan dalam format data serial tidak cocok dengan nama properti dari tipe data Anda, Anda dapat mengimplementasikan enum ini secara manual dan menyetel yang sesuai rawValueuntuk kasus yang diperlukan.

Contoh berikut menunjukkan bagaimana melakukannya:

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 (mengganti zipproperti dengan kunci 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"}
 */

Decode (mengganti kunci JSON "zip_code" dengan zipproperti):

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. Menggunakan strategi kode kasus ular untuk kasus unta

Jika JSON Anda memiliki tombol ular-cased dan Anda ingin mengkonversikannya ke sifat unta-cased untuk objek model Anda, Anda dapat mengatur Anda JSONEncoder's keyEncodingStrategydan JSONDecoder' s keyDecodingStrategyproperti untuk .convertToSnakeCase.

Contoh berikut menunjukkan bagaimana melakukannya:

import Foundation

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

Enkode (mengonversi properti selubung unta menjadi kunci JSON bersarung ular):

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

Dekode (mengonversi kunci JSON yang disimpan dalam kotak ular menjadi properti kasing unta):

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. Menggunakan strategi pengkodean kunci khusus

Jika perlu, JSONEncoderdan JSONDecodermemungkinkan Anda untuk mengatur strategi kustom untuk memetakan kunci pengkodean menggunakan JSONEncoder.KeyEncodingStrategy.custom(_:)dan JSONDecoder.KeyDecodingStrategy.custom(_:).

Contoh berikut menunjukkan cara menerapkannya:

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 (mengonversi properti huruf pertama huruf kecil menjadi huruf besar pertama kunci JSON):

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

Dekode (mengubah kunci JSON huruf besar pertama menjadi properti huruf pertama huruf kecil):

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

Sumber:

Imanou Petit
sumber
3

Apa yang telah saya lakukan adalah membuat struktur sendiri seperti yang Anda dapatkan dari JSON sehubungan dengan tipe datanya.

Seperti ini:

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
}

Setelah ini, Anda perlu membuat ekstensi yang sama structmemperluas decodabledan enumstruktur yang sama dengan CodingKeydan kemudian Anda perlu untuk menginisialisasi decoder menggunakan enum ini dengan tombol dan tipe data (Keys akan datang dari enum dan tipe data akan datang atau katakanlah dirujuk dari struktur itu sendiri)

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

Anda perlu mengubah di sini setiap kunci dan tipe data sesuai dengan kebutuhan Anda dan menggunakannya dengan decoder.

Tushar
sumber
-1

Dengan menggunakan CodingKey, Anda dapat menggunakan kunci khusus dalam protokol yang dapat dikodekan atau didekodekan.

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
sumber