首页 文章

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

提问于
浏览
56

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

3 回答

  • 9

    手动自定义编码密钥

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

    但是,这种自动生成的一致性的一个非常巧妙的特性是,如果您在类型中定义了一个名为“ CodingKeys ”(或使用带有此名称的 typealias )符合CodingKey协议的嵌套 enum ,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一致性的确切规则详见the evolution proposal(强调我的):

    除了枚举的自动CodingKey需求合成之外,还可以为某些类型自动合成Encodable&Decodable要求:符合Encodable的类型,其属性都是Encodable,可以获得自动生成的String-backed CodingKey枚举映射属性到案例名称 . 类似地,对于Decodable类型,其属性都是可解码类型属于(1) - 以及手动提供CodingKey枚举的类型(直接或通过typealias命名为CodingKeys),其案例按名称将1对1映射到Encodable / Decodable属性 - 获取init的自动合成(从:)和编码(到:)视情况而定,使用那些属性和键属于既不属于(1)也不属于(2)的类型必须提供自定义键类型(如果需要)并提供自己的init(从:)和编码(到:),视情况而定

    编码示例:

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

    camelCase属性名称的自动snake_case JSON密钥

    在Swift 4.1中,如果将 zip 属性重命名为 zipCode ,则可以利用 JSONEncoderJSONDecoder 上的密钥编码/解码策略,以便在 camelCasesnake_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中,您可以利用 JSONEncoderJSONDecoder 上的自定义键编码/解码策略,允许您提供自定义函数来映射编码键 .

    您提供的函数采用 [CodingKey] ,它表示编码/解码中当前点的编码路径(在大多数情况下,您只需要考虑最后一个元素;即当前键) . 该函数返回 CodingKey ,它将替换此数组中的最后一个键 .

    例如, UpperCamelCase 属性名称的 UpperCamelCase 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")
    
  • 0

    使用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 .

    以下示例显示了如何执行:

    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具有蛇形键并且您想将它们转换为模型对象的驼峰属性,则可以将 JSONEncoderkeyEncodingStrategyJSONDecoderkeyDecodingStrategy属性设置为 .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 . 使用自定义密钥编码策略

    如有必要, 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
        }
    }
    
    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")
     */
    

    资料来源:

  • 132

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

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

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

相关问题