如何在Swift 4的可解码协议中使用自定义键?

时间:2017-06-06 17:42:27

标签: json swift swift4 codable

Swift 4通过Decodable协议引入了对原生JSON编码和解码的支持。如何使用自定义键?

例如,我说有一个结构

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协议,但我无法弄清楚如何使用它。

4 个答案:

答案 0 :(得分:165)

手动自定义编码密钥

在您的示例中,您获得了自动生成的Codable一致性,因为您的所有属性也符合Codable。这种一致性自动创建一个简单对应于属性名称的密钥类型 - 然后用于从单个密钥容器进行编码/解码。

然而,这种自动生成的一致性的一个真正整洁的功能是,如果您在类型中定义了一个名为" enum"的嵌套CodingKeys。 (或使用具有此名称的typealias)符合CodingKey协议 - Swift将自动使用 this 作为密钥类型。因此,您可以轻松自定义使用编码/解码属性的键。

所以这意味着你可以说:

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

枚举案例名称需要与属性名称匹配,并且这些案例的原始值需要匹配您要编码/解码的键(除非另有说明,否则为{{1的原始值)枚举将与案例名称相同)。因此,String属性现在将使用键zip进行编码/解码。

自动生成的Encodable / Decodable一致性的确切规则详见the evolution proposal(强调我的):

  

除了自动"zip_code"需求合成之外   CodingKeyenums& Encodable要求可以自​​动生效   合成某些类型:

     
      
  1. 符合Decodable且其属性均为Encodable的类型获得自动生成的Encodable - 支持的String枚举映射   案例名称的属性。同样适用于CodingKey类型   属性都是Decodable

  2.   
  3. 属于(1) - 的类型以及直接或通过Decodable手动提供CodingKey enum(名为CodingKeys的类型) 谁的   案例按名称将1对1映射到typealias / Encodable属性 - 获取   酌情自动合成Decodableinit(from:),   使用这些属性和键

  4.   
  5. 既不属于(1)也不属于(2)的类型必须提供自定义密钥类型(如果需要)并提供自己的encode(to:)和   init(from:),视情况而定

  6.   

编码示例:

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密钥

在Swift 4.1中,如果您将camelCase属性重命名为zip,则可以利用zipCodeJSONEncoder上的关键编码/解码策略,以便自动转换JSONDecodercamelCase之间的编码密钥。

编码示例:

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

关于这个策略需要注意的一个重要事项是,它无法使用首字母缩略词或首字母缩写词来往返一些属性名称,根据Swift API design guidelines,它应该统一为高或低案例(取决于职位)。

例如,名为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")的属性将使用密钥someURL进行编码,但在解码时,会将其转换为some_url

要解决此问题,您必须手动将该属性的编码密钥指定为解码器所需的字符串,例如someUrl在这种情况下(仍会转换为someUrl编码器):

some_url

(这并没有严格回答你的具体问题,但考虑到这个Q& A的规范性质,我认为值得包括)

自定义自动JSON密钥映射

在Swift 4.1中,您可以利用struct S : Codable { private enum CodingKeys : String, CodingKey { case someURL = "someUrl", someOtherProperty } var someURL: String var someOtherProperty: String } 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

答案 1 :(得分:12)

使用Swift 4.2,根据您的需要,您可以使用以下3种策略之一,以使您的模型对象自定义属性名称与您的JSON键匹配。

#1。使用自定义编码密钥

当您使用以下实现声明符合CodableDecodableEncodable协议)的结构时...

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。 p>

以下示例说明了如何执行:

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_code" JSON键替换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"}
 */

解码(替换" zip_code"带有zip属性的JSON密钥):

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和{{1将keyDecodingStrategy属性JSONDecoder添加到.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。使用自定义密钥编码策略

如有必要,JSONEncoderJSONDecoder可让您设置自定义策略,以使用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")
 */

来源:

答案 2 :(得分:2)

我所做的就是创建自己的结构,就像您从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的扩展名,然后初始化解码器将此枚举及其键和数据类型一起使用(键将来自枚举,而数据类型将来自结构本身,或者说是从结构本身引用的)

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

您需要在此处根据需要更改每个键和数据类型,并将其与解码器一起使用。

答案 3 :(得分:0)

通过使用 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
    } }