如何在 Swift 4 的可解码协议中使用自定义键?
Posted
技术标签:
【中文标题】如何在 Swift 4 的可解码协议中使用自定义键?【英文标题】:How do I use custom keys with Swift 4's Decodable protocol? 【发布时间】: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
协议,但我不知道如何使用它。
【问题讨论】:
【参考方案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
要求可以自动 也为某些类型合成:
符合
Encodable
且属性全部为Encodable
的类型获取自动生成的String
支持的CodingKey
枚举映射 案例名称的属性。类似的Decodable
类型 属性都是Decodable
属于 (1) 的类型 — 以及手动提供
CodingKey
enum
(直接命名为CodingKeys
,或通过typealias
)的类型,其 case 按名称一对一映射到Encodable
/Decodable
属性 — 获取 视情况自动合成init(from:)
和encode(to:)
, 使用这些属性和键既不属于 (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_case
camelCase
属性名称的自动 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
,它将替换此数组中的最后一个键。
例如,UpperCamelCase
lowerCamelCase
属性名称的 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")
*/
来源:
Apple developer documentation: "Encoding and Decoding Custom Types" WWDC 2017 session 212: "What's new in Foundation" MartianCraft: "Implementing a custom key strategy for coding types"【讨论】:
【参考方案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
【讨论】:
以上是关于如何在 Swift 4 的可解码协议中使用自定义键?的主要内容,如果未能解决你的问题,请参考以下文章
如何在 Swift 中使用动态键(在根级别)解码此 JSON?
将 JSON 数据从 Parse Cloud Code 返回到 Swift 中的可解码结构