【问题标题】:How do I use custom keys with Swift 4's Decodable protocol?如何在 Swift 4 的可解码协议中使用自定义键?
【发布时间】:2017-11-07 20:29:18
【问题描述】:

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 协议,但我不知道如何使用它。

【问题讨论】:

    标签: json swift swift4 codable


    【解决方案1】:

    手动自定义编码键

    在您的示例中,您将自动生成符合Codable,因为您的所有属性也符合Codable。这种一致性会自动创建一个与属性名称相对应的键类型,然后使用该类型对单个键容器进行编码/解码。

    但是,这种自动生成的一致性的一个真正巧妙的特性是,如果您在名为“CodingKeys”的类型中定义一个嵌套的enum(或使用具有此名称的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
        }
    }
    

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

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

    除了自动CodingKey 需求综合 enums, Encodable & Decodable 要求可以自​​动 也为某些类型合成:

    1. 符合Encodable且属性全部为Encodable的类型获取自动生成的String支持的CodingKey枚举映射 案例名称的属性。类似的Decodable 类型 属性都是Decodable

    2. 属于 (1) 的类型 — 以及手动提供 CodingKey enum(直接命名为 CodingKeys,或通过 typealias)的类型,其 case 按名称一对一映射到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_casecamelCase 属性名称的自动 snake_case JSON 键

    在 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,它将替换此数组中的最后一个键。

    例如,UpperCamelCaselowerCamelCase 属性名称的 JSON 键:

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

    【讨论】:

    • 我自己偶然发现了这个!我想知道,有没有办法只覆盖我想要更改的一个键而让其余的不理会?例如。在 case 语句中,在 CodingKeys 枚举下;我可以只列出我正在更改的一个键吗?
    • """ 用于 多行 文字:)
    • @MartinR 或者甚至只是一行文字而无需转义 "s :D
    • @chrismanderson 完全正确——特别是考虑到编译器强制案例名称与属性名称保持同步(否则它会给你一个错误,说你不符合Codable)
    • @ClayEllis 啊,是的,当然,例如直接在Address 的初始化程序中直接使用嵌套容器会不必要地将自己与解码从父对象图中特定位置开始的 JSON 对象联系起来。将起始密钥路径抽象到解码器本身会更好——这里是a rough hackey-ish implementation。
    【解决方案2】:

    使用 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_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 属性替换“zip_code”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。使用snake case to camel case的关键编码策略

    如果您的 JSON 具有蛇形密钥,并且您希望将它们转换为模型对象的驼峰形属性,您可以将 JSONEncoder 的 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
      }
      

      在此之后,您需要创建与 CodingKey 相同结构的相同 struct 扩展 decodable 和 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 = ""
              }
          }
      }
      

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

      【讨论】:

        【解决方案4】:

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

        【讨论】:

          猜你喜欢
          • 2020-01-31
          • 2018-03-31
          • 1970-01-01
          • 1970-01-01
          • 2018-10-30
          • 2014-07-24
          • 2020-06-17
          • 1970-01-01
          • 2018-04-01
          相关资源
          最近更新 更多