Swift 4'ün Çözülebilir protokolü ile özel anahtarları nasıl kullanırım?


102

Swift 4, Decodableprotokol aracılığıyla yerel JSON kodlama ve kod çözme desteği sundu . Bunun için özel anahtarları nasıl kullanırım?

Örneğin, bir yapım olduğunu varsayalım

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

Bunu JSON olarak kodlayabilirim.

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

Bunu bir nesneye geri kodlayabilirim.

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

Ama bir json nesnem olsaydı

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

AddressBu zip_codeharitalardaki şifre çözücüye nasıl söylerim zip? Yeni CodingKeyprotokolü kullandığına inanıyorum , ama bunu nasıl kullanacağımı bulamıyorum.

Yanıtlar:


258

Kodlama anahtarlarını manuel olarak özelleştirme

Örneğinizde Codable, tüm mülklerinizin de uyduğu için otomatik olarak oluşturulmuş bir uyum elde ediyorsunuz Codable. Bu uygunluk otomatik olarak özellik adlarına karşılık gelen bir anahtar türü oluşturur - bu daha sonra tek bir anahtarlı kapsayıcıdan kodlama / kod çözme için kullanılır.

Ancak tek gerçekten otomatik olarak oluşturulan bu uygunluk özellik düzgün olduğunu bir yuvalanmış tanımlarsanız enum"adlı tipinde CodingKeys" (ya da kullanmak typealiasiçin uygundur bu isimde) CodingKeyprotokolü - Swift otomatik kullanacağı bu anahtar türü olarak. Bu nedenle, özelliklerinizin kodlandığı / kodunun çözüldüğü anahtarları kolayca özelleştirmenize olanak tanır.

Yani bunun anlamı şudur:

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

Enum vaka adlarının özellik adlarıyla eşleşmesi gerekir ve bu vakaların ham değerlerinin kodladığınız / kod çözdüğünüz anahtarlarla eşleşmesi gerekir (aksi belirtilmedikçe, Stringnumaralandırmanın ham değerleri vaka adlarıyla aynı olacaktır. ). Bu nedenle, zipözellik artık anahtar kullanılarak kodlanacak / kodu çözülecektir "zip_code".

Otomatik oluşturulan Encodable/ Decodableuygunluk için kesin kurallar , evrim önerisinde ayrıntılı olarak belirtilmiştir (vurgu benim):

Otomatik CodingKeygereksinim sentezine ek olarak enums, Encodable& Decodablegereksinimler belirli türler için de otomatik olarak sentezlenebilir:

  1. Uygun tipleri Encodable, özellikleri hepsi Encodableotomatik olarak oluşturulmuş olsun String-backed CodingKeyvaka adlarına enum haritalama özellikleri. Benzer şekilde Decodabletüm özellikleri olan türler içinDecodable

  2. (1) 'e düşen türlerCodingKey enumCodingKeystypealiasEncodableDecodable - ve durumları 1'e 1'i / özellikleri ada göre eşleyen manuel olarak (adlandırılmış , doğrudan veya a yoluyla ) sağlayan türler - bu özelliklerin ve anahtarların otomatik sentezini init(from:)ve encode(to:)uygun şekilde elde edin

  3. Ne (1) ne de (2) kapsamına giren türlerin, gerekirse özel bir anahtar türü sağlaması ve kendi anahtarlarını init(from:)ve encode(to:)uygun şekilde

Örnek kodlama:

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

Örnek kod çözme:

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

Özellik adları snake_caseiçin otomatik JSON anahtarlarıcamelCase

Eğer adlandırmak eğer Swift 4.1 yılında zipmülkü zipCode, üzerinde stratejiler deşifre / anahtar kodlama yararlanabilir JSONEncoderve JSONDecoderotomatik amacıyla arasındaki kodlama anahtarları dönüştürmek camelCaseve snake_case.

Örnek kodlama:

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

Örnek kod çözme:

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

Bununla birlikte, bu strateji hakkında dikkat edilmesi gereken önemli bir nokta, Swift API tasarım yönergelerine göre (konuma bağlı olarak tekdüze olarak büyük veya küçük harf olması gereken kısaltmalar veya baş harflerle bazı özellik adlarını geri alamayacağıdır). ).

Örneğin, adı verilen bir özellik someURLanahtarla kodlanacak some_url, ancak kod çözüldüğünde bu dönüştürülecektir someUrl.

Bunu düzeltmek için, bu özelliğin kodlama anahtarını kod çözücünün beklediği dize olacak şekilde manuel olarak belirtmeniz gerekir, örneğin someUrlbu durumda ( some_urlkodlayıcı tarafından hala dönüştürülecektir ):

struct S : Codable {

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

  var someURL: String
  var someOtherProperty: String
}

(Bu, sorunuzu kesin olarak yanıtlamaz, ancak bu Soru-Cevap bölümünün kanonik doğası göz önüne alındığında, buna değer olduğunu düşünüyorum)

Özel otomatik JSON anahtar eşleme

Swift 4.1'de, özel anahtar kodlama / kod çözme stratejilerinden faydalanabilir JSONEncoderve JSONDecoderkodlama anahtarlarını eşlemek için özel bir işlev sağlamanıza olanak tanır.

Sağladığınız işlev [CodingKey], kodlama / kod çözmede geçerli noktanın kodlama yolunu temsil eden a alır (çoğu durumda, yalnızca son öğeyi, yani geçerli anahtarı dikkate almanız gerekir). İşlev CodingKey, bu dizideki son anahtarın yerini alacak bir a döndürür .

Örneğin, özellik adları UpperCamelCaseiçin JSON anahtarları lowerCamelCase:

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

Artık .convertToUpperCamelCaseanahtar strateji ile kodlayabilirsiniz :

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

ve .convertFromUpperCamelCaseanahtar strateji ile kod çözme :

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

Sadece buna kendim rastladım! Merak ediyorum, değiştirmek istediğim tek anahtarı geçersiz kılmanın ve gerisini yalnız bırakmanın bir yolu var mı? Örneğin case deyiminde, CodingKeysenum altında ; sadece değiştirdiğim anahtarı listeleyebilir miyim?
chrismanderson

2
"""Bir için çok hatlı :) Değişmez
Martin R

6
@MartinR Veya kaçmak zorunda kalmadan bile sadece tek bir satır değişmezi "s: D
Hamish

1
@chrismanderson Kesinlikle - özellikle derleyicinin vaka adlarının mülk isimleriyle eşzamanlı tutulmasını zorunlu kıldığı göz önüne alındığında ( Codableaksi takdirde uymadığınızı söyleyen bir hata verecektir )
Hamish

1
@ClayEllis Ah evet, tabii ki iç içe geçmiş konteynerleri örneğin doğrudan başlatıcıda kullanmak Addressgereksiz yere üst nesne grafiğinde belirli bir yerde başlayan bir JSON nesnesinin kodunu çözmeye bağlı. Başlangıç ​​anahtarı yolunu kod çözücünün kendisine kadar soyutlamak çok daha hoş olurdu - işte kaba bir hackey-ish uygulaması .
Hamish

17

Swift 4.2 ile ihtiyaçlarınıza göre, model nesnelerinizin özel özellik adlarının JSON anahtarlarınızla eşleşmesini sağlamak için aşağıdaki 3 stratejiden birini kullanabilirsiniz.


# 1. Özel kodlama anahtarlarını kullanma

Aşağıdaki uygulamaya Codable( Decodableve Encodableprotokollere) uyan bir yapı beyan ettiğinizde ...

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

... derleyici otomatik olarak CodingKeysizin için protokole uyan bir iç içe enum oluşturur .

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

Bu nedenle, serileştirilmiş veri biçiminizde kullanılan anahtarlar veri türünüzdeki özellik adlarıyla eşleşmiyorsa, bu numaralandırmayı manuel olarak uygulayabilir rawValueve gerekli durumlar için uygun olanı ayarlayabilirsiniz .

Aşağıdaki örnek nasıl yapılacağını gösterir:

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

Kodlama ( zipözelliği "zip_code" JSON anahtarıyla değiştirme):

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

Kod çözme ("zip_code" JSON anahtarını zipözellik ile değiştirme ):

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. Deve vakası anahtar kodlama stratejileri için yılan kasasını kullanma

JSON'nizde yılan kasalı anahtarlar varsa ve bunları model nesneniz için deve kasalı özelliklere dönüştürmek istiyorsanız, JSONEncoder's keyEncodingStrategyve JSONDecoder' nin keyDecodingStrategyözelliklerini olarak ayarlayabilirsiniz .convertToSnakeCase.

Aşağıdaki örnek nasıl yapılacağını gösterir:

import Foundation

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

Kodlama (deve kasalı özellikleri yılan kasalı JSON anahtarlarına dönüştürme):

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

Kod çözme (yılan kasalı JSON anahtarlarını deve kasalı özelliklere dönüştürme):

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. Özel anahtar kodlama stratejilerini kullanma

Gerekirse, JSONEncoderve JSONDecoderkullandığınız tuşları kodlama eşlemek için bir özel strateji belirlemesine izin JSONEncoder.KeyEncodingStrategy.custom(_:)ve JSONDecoder.KeyDecodingStrategy.custom(_:).

Aşağıdaki örnek, bunların nasıl uygulanacağını gösterir:

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

Kodlama (küçük harfli ilk harf özelliklerini büyük harfli ilk harf JSON anahtarlarına dönüştürme):

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

Kod çözme (büyük harfli ilk harf JSON anahtarlarını küçük harfli ilk harf özelliklerine dönüştürme):

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

Kaynaklar:


3

Yaptığım şey, veri türleri açısından JSON'dan aldığınız gibi kendi yapınızı oluşturmaktır.

Aynen böyle:

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
}

Bundan sonra aynı bir uzantısı oluşturmak için gereken structuzanan decodableve enumaynı yapının CodingKeyve daha sonra onun tuşları ve veri türleri ile bu enum kullanarak dekoder ilklendirmelisiniz (Keys enum gelecek ve veri türleri geliyor veya diyelim olacak yapının kendisinden referans alınmıştır)

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

Burada her bir anahtarı ve veri türünü ihtiyaçlarınıza göre değiştirmeniz ve kod çözücü ile kullanmanız gerekir.


-1

CodingKey kullanarak , kodlanabilir veya kodu çözülebilir protokollerde özel anahtarlar kullanabilirsiniz.

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
    } }
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.