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

146

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

4个回答

358

手动自定义编码键

在你的示例中,由于你的所有属性也符合 Codable,因此你会获得一个自动生成的符合 Codable 的一致性。这个一致性自动创建了一个键类型,它简单地对应于属性名称-然后用于编码/解码一个单一的键控容器。

然而,这个自动生成的一致性中真正聪明的特性之一是,如果你在你的类型中定义了一个名为 "CodingKeys" 的嵌套 enum(或使用这个名称的 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,某些类型的EncodableDecodable需求也可以自动合成:
  1. 符合属性全部为EncodableEncodable类型获得一个自动生成的String支持的CodingKey枚举映射,将属性映射到case名称。类似地,对于所有属性都是DecodableDecodable类型也是如此。

  2. 落入(1)中的类型 - 以及手动提供CodingKeyenum(直接命名为CodingKeys或通过typealias)并按名称将情况映射到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")

自动将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()
  <b>encoder.keyEncodingStrategy = .convertToSnakeCase</b>
  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()
  <b>decoder.keyDecodingStrategy = .convertFromSnakeCase</b>
  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中,您可以利用JSONEncoderJSONDecoder上的自定义键编码/解码策略,从而为编码键提供自定义函数。

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

例如,将lowerCamelCase属性名称的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")

2
"""用于多行文字的字面值 :) - Martin R
6
甚至只需一行文字,无需转义"即可:@MartinR - Hamish
1
@chrismanderson 没错 - 特别是考虑到编译器强制要求案例名称与属性名称保持同步(否则它会给出错误,指出您未遵守 Codable)。 - Hamish
1
@ClayEllis 是的,虽然当然可以直接在Address的初始化器中使用嵌套容器作为示例,但这样做会不必要地将您自己绑定到解码从父对象图中特定位置开始的JSON对象。更好的方法是将起始键路径抽象到解码器本身 - 这里是一个粗略的hackey-ish实现 - Hamish
1
@c1pherB1t 不用担心!codingKeys.last会给你当前的编码键,数组中所有先前的元素都是给定编码路径的父编码键。例如,在解码{"foo": {"bar": "baz"}}时,当应用键解码策略到"bar"时,codingKeys将是["foo", "bar"] - Hamish
显示剩余19条评论

37

在 Swift 4.2 中,根据您的需要,您可以使用以下三种策略之一,使您的模型对象自定义属性名称与您的 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
    }
}

编码(将zip属性替换为“zip_code” JSON键):

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具有蛇形命名的键,而您想将它们转换为驼峰式属性以用于您的模型对象中,您可以将您的JSONEncoderkeyEncodingStrategyJSONDecoderkeyDecodingStrategy属性设置为.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")
 */

资料来源:


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的扩展程序,以及具有CodingKey的相同结构的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 = ""
        }
    }
}

您需要根据自己的需求更改每个键和数据类型,并与解码器一起使用。


或者您可以使用decodeIfPresentlabel = trackContainer.decodeIfPresent(String.self, forKey: .label) ?? "" - ice-wind

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接