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

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协议,但我不知道怎么用。

83314 次浏览

手动定制编码键

在您的示例中,您将获得与 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
}
}

枚举大小写名称需要与属性名称匹配,这些大小写的原始值需要与您编码到/解码的键匹配(除非另有说明,否则 String枚举的原始值将与大小写名称相同)。因此,现在将使用键 "zip_code"zip属性进行编码/解码。

自动生成的 Encodable/Decodable一致性的确切规则在 进化计划中有详细介绍(重点介绍) :

除了自动 CodingKey需求综合之外 enumsEncodableDecodable的要求可以自动 也为某些类型合成:

  1. 符合其所有属性都是 EncodableEncodable类型将获得一个自动生成的、支持 StringCodingKey枚举映射 属性转换为大小写名称。类似地,对于其 属性都是 Decodable

  2. 属于(1)的类型ーー < strong > 和手动提供 CodingKey enum(命名为 CodingKeys,直接或通过 typealias)的类型 Case 通过 name 映射1-to-1到 Encodable/Decodable属性ー get 适当时自动合成 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()
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 设计指南,这些缩写或首字母缩写应该是大写或小写(取决于位置)。

例如,名为 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")

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


使用自定义编码键

当您使用以下实现声明一个符合 Codable(DecodableEncodable协议)的结构时..。

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

Decode (用 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. 使用蛇格来实现骆驼格密钥编码策略

如果您的 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")
*/

使用自定义密钥编码策略

如果需要,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")
*/

资料来源:

我所做的就是创建自己的结构,就像您从 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的扩展,扩展 decodableenum的相同结构和 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 = ""
}
}
}

您需要根据您的需要在这里更改每个密钥和数据类型,并将其与解码器一起使用。

通过使用 编码钥匙,您可以在可编码或可解码协议中使用自定义密钥。

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