Как я могу использовать пользовательские ключи с протоколом Свифт 4-х Декодируемые?


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 протокол, но я не могу понять, как это использовать.

2 51

2 ответа:

ручная настройка ключей кодирования

в вашем примере, вы получаете автоматически сгенерированный соответствия Codable так как все ваши свойства также соответствуют Codable. Это соответствие автоматически создает тип ключа, который просто соответствует именам свойств – которые затем используются для кодирования/декодирования из одного контейнера с ключом.

один действительно особенность этого авто-сгенерированный соответствия заключается в том, что если вы определяете вложенный enum в вашем типе называется"CodingKeys" (или использовать 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
    }
}

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

точные правила для автоматически генерируемых Encodable/Decodable соответствие детализировано эволюция предложение (выделено мной):

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

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

  2. типы, попадающие в (1) - и типы, которые вручную предоставляют CodingKeyenum (название CodingKeys, напрямую, или через 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 Design guidelines, следует равномерно верхний или нижний регистр (в зависимости от позиции).

например, свойство с именем 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, что позволяет предоставить пользовательскую функцию для отображения ключей кодирования.

функция, которую вы предоставляете, принимает [CodingKey], который представляет путь кодирования для текущей точки кодирования / декодирования (в большинстве случаев вам нужно будет рассмотреть только последний элемент; то есть текущий ключ). Функция возвращает значение 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")

С 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
    }
}
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"}
 */
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 есть ключи в змеиной оболочке, и вы хотите чтобы преобразовать их в свойства camel-cased для вашего объекта модели, вы можете установить JSONEncoder ' s keyEncodingStrategy и JSONDecoder ' s keyDecodingStrategy свойства .convertToSnakeCase.

в следующем примере показано, как это сделать:

import Foundation

struct Address: Codable {
    var street: String
    var zipCode: String
    var cityName: String
    var state: String
}
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"}
 */
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
    }
}
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"}
 */
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")
 */

источники: