Як використовувати користувацькі ключі з протоколом розшифровки Swift 4?


102

Swift 4 представив підтримку нативного кодування та декодування JSON через Decodableпротокол. Як використовувати для цього спеціальні ключі?

Скажімо, у мене є структура

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

Я можу це кодувати 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" 
           }
    }
}

Я можу це кодувати назад до об'єкта.

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

Але якби у мене був об’єкт json, який був

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

Як мені сказати декодер на Addressцій zip_codeкарті zip? Я вважаю, ви використовуєте новий CodingKeyпротокол, але я не можу зрозуміти, як це використовувати.

Відповіді:


258

Налаштування ключів кодування вручну

У вашому прикладі ви отримуєте автоматично створену відповідність тому, Codableяк відповідають і всі ваші властивості Codable. Ця відповідність автоматично створює тип ключа, який просто відповідає іменам властивостей - який потім використовується для кодування та декодування з одного контейнера з ключем.

Однак одна з дійсно акуратних властивостей цього автоматично сформованого відповідності полягає в тому, що якщо ви визначите вкладений enumу своєму типі під назвою " CodingKeys" (або використовуєте a typealiasз цим ім'ям), що відповідає CodingKeyпротоколу, - Swift автоматично використовуватиме це як тип ключа. Тому це дозволяє легко налаштувати ключі, якими ваші властивості кодуються / декодуються.

Отже, що це означає, ви можете просто сказати:

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

Імена перерахування випадку повинні збігатися з іменами властивостей, а вихідні значення цих випадків повинні відповідати ключам , які ви кодують к / декодування з (якщо не вказано інших, вихідних значень в Stringперерахуванні буде таким же , як імена конкретних ). Тому zipвластивість тепер буде закодовано / декодовано за допомогою ключа "zip_code".

Точні правила для автоматичного генерування Encodable/ Decodableвідповідності детально описані в еволюційній пропозиції (міна акценту):

На додаток до автоматичного CodingKeyсинтезу вимог до enums, Encodable& Decodableвимоги також можуть бути автоматично синтезовані для певних типів:

  1. Типи, які відповідають Encodableвсім властивостям, Encodableотримують автоматично створені, Stringзахищені переліку переліку CodingKeyвластивості перерахування до імен справ. Аналогічно для Decodableтипів, властивості яких усіDecodable

  2. Типи, що потрапляють у (1) - та типи, які надають вручну CodingKey enum(названі CodingKeys, безпосередньо чи через a typealias), випадки яких відображають 1-до-1 в Encodable/ Decodableвластивості за назвою - отримують автоматичний синтез init(from:)та, encode(to:)якщо потрібно, використовуючи ці властивості та ключі

  3. Типи, які не належать ні (1), ні (2), не потребують надання необхідного типу ключа, якщо потрібно, та надають свій власний init(from:)та encode(to:), за необхідності

Приклад кодування:

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

Приклад декодування:

// 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_caseклавіші JSON для camelCaseімен властивостей

У Swift 4.1, якщо ви перейменовуєте свій zipресурс у zipCode, ви можете скористатися стратегіями кодування / декодування ключів у JSONEncoderта JSONDecoderдля автоматичного перетворення ключів кодування між camelCaseі snake_case.

Приклад кодування:

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

Приклад декодування:

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

Але важливо, що слід зазначити щодо цієї стратегії, це те, що вона не зможе обирати деякі імена властивостей з абревіатурами або ініціалізмами, які, згідно з інструкціями щодо дизайну Swift API , повинні бути рівномірно верхній або нижній регістр (залежно від позиції ).

Наприклад, властивість з ім'ям someURLбуде закодовано ключем some_url, але при декодуванні це буде перетворено в someUrl.

Щоб виправити це, вам доведеться вручну вказати ключ кодування для цього властивості як рядок, який очікує декодер, наприклад, someUrlу цьому випадку (який все ще буде перетворений some_urlкодером):

struct S : Codable {

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

  var someURL: String
  var someOtherProperty: String
}

(Це не відповідає чітко на ваше конкретне запитання, але, враховуючи канонічність цього питання, я вважаю, що це варто включити)

Налаштування автоматичного відображення ключа JSON

У Swift 4.1 ви можете скористатись стратегіями кодування / декодування користувальницьких ключів JSONEncoderта JSONDecoder, що дозволяють вам надати спеціальну функцію для відображення ключів кодування.

Функція, яку ви надаєте, приймає a [CodingKey], який представляє шлях кодування для поточної точки кодування / декодування (у більшості випадків вам потрібно буде враховувати лише останній елемент; тобто поточний ключ). Функція повертає a, CodingKeyщо замінить останній ключ у цьому масиві.

Наприклад, UpperCamelCaseключі JSON для 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
    }
  }
}

Тепер ви можете кодувати .convertToUpperCamelCaseключову стратегію:

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

і декодувати за допомогою .convertFromUpperCamelCaseключової стратегії:

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

Просто натрапив на це сам! Цікаво, чи існує спосіб змінити лише один ключ, який я хочу змінити, а решту залишити в спокої? Наприклад, у заяві справи під CodingKeysперерахунком; чи можу я просто перерахувати один ключ, який я змінюю?
chrismanderson

2
"""для багатолінійного буквального :)
Martin R

6
@MartinR Або навіть лише однолінійний літерал без необхідності бігти "s: D
Hamish

1
@chrismanderson Саме так - особливо зважаючи на те, що компілятор накладає на те, щоб імена справ синхронізувалися з іменами властивостей (це дасть вам помилку, сказавши, що ви не відповідаєте Codableіншому)
Hamish

1
@ClayEllis Так, хоча, звичайно, використовуючи вкладені контейнери, наприклад, безпосередньо в ініціалізаторі, Addressнепотрібно зв’язує себе з розшифровкою об'єкта JSON, який починається з певного місця в графі батьківського об'єкта. Було б набагато приємніше абстрагувати вихідний ключовий шлях до самого декодера - ось груба реалізація хакею .
Гаміш

17

З Swift 4.2, відповідно до ваших потреб, ви можете використовувати одну з 3 наступних стратегій, щоб змусити власні об’єкти моделей власних властивостей відповідати вашим клавішам JSON.


№1. Використання спеціальних ключів кодування

Коли ви заявляєте структуру, яка відповідає Codable( Decodableта Encodableпротоколам) із наступною реалізацією ...

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

... компілятор автоматично генерує вкладений перелік, який відповідає CodingKeyпротоколу для вас.

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

Тому, якщо ключі, використовувані у вашому серіалізованому форматі даних, не відповідають іменам властивостей вашого типу даних, ви можете вручну реалізувати цей перелік та встановити відповідні rawValueдля необхідних випадків.

Наступний приклад показує, як це зробити:

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

Кодування (заміна zipвластивості ключем 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"}
 */

Розшифруйте (замінивши "zip_code" ключ JSON zipвластивістю):

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. Використання корпусу змії для стратегії кодування ключових випадків верблюда

Якщо у вашому JSON є ключі зі змією, і ви хочете перетворити їх у властивості, пов'язані з верблюдом, для вашого модельного об'єкта, ви можете встановити для своїх властивостей JSONEncoder's keyEncodingStrategyі JSONDecoder' .keyDecodingStrategy.convertToSnakeCase

Наступний приклад показує, як це зробити:

import Foundation

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

Кодування (перетворення властивостей, пов'язаних з верблюдами, у клавіші JSON):

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

Декодування (перетворення ключів JSON, що перебуває під змією, у властивості, які мають верблюд):

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. Використання спеціальних стратегій кодування ключів

Якщо необхідно, JSONEncoderі JSONDecoderдозволяють встановити власну стратегію для відображення ключів кодування за допомогою JSONEncoder.KeyEncodingStrategy.custom(_:)і JSONDecoder.KeyDecodingStrategy.custom(_:).

Наступний приклад показує, як їх реалізувати:

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

Кодування (перетворення властивостей першої літери нижнього розміру у великі великі клавіші 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"}
 */

Декодування (перетворення великих літер клавіш JSON першої літери у властивості першої літери нижнього розміру):

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

Джерела:


3

Що я зробив, це створити власну структуру, як і те, що ви отримуєте від JSON щодо його типів даних.

Просто так:

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
}

Після цього вам потрібно створити розширення того ж structрозширення decodableта тієї enumсамої структури з, CodingKeyа потім вам потрібно ініціалізувати декодер за допомогою цього enum з його ключами та типами даних (ключі будуть надходити з enum, а типи даних будуть надходити або говорити посилання з самої структури)

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

Вам потрібно змінити тут кожен ключ і типи даних відповідно до ваших потреб і використовувати його з декодером.


-1

Використовуючи CodingKey, ви можете використовувати спеціальні ключі в кодируемом або декодируемом протоколі.

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
    } }
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.