字典的可解码 keyDecodingStrategy 自定义处理
Decodable keyDecodingStrategy custom handling for dictionaries
我有以下 JSON 对象:
{
"user_name":"Mark",
"user_info":{
"b_a1234":"value_1",
"c_d5678":"value_2"
}
}
我的 JSONDecoder
是这样设置的:
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
我的 Decodable
对象看起来像这样:
struct User: Decodable {
let userName: String
let userInfo: [String : String]
}
我面临的问题是 .convertFromSnakeCase
策略正在应用于字典的键,我希望这种情况不会发生。
// Expected Decoded userInfo
{
"b_a1234":"value_1",
"c_d5678":"value_2"
}
// Actual Decoded userInfo
{
"bA1234":"value_1",
"cD5678":"value_2"
}
我研究过使用自定义 keyDecodingStrategy
(但没有足够的信息来以不同方式处理字典),以及我的 Decodable
结构的自定义初始化程序(似乎键有此时已经转换)。
正确的处理方法是什么(仅为字典创建键转换异常)?
注意:我宁愿保留蛇形转换策略,因为我的实际 JSON 对象在蛇形中有很多属性。我当前的解决方法是使用 CodingKeys 枚举手动进行蛇形转换。
是的......但是,这有点棘手,最后可能只是添加 CodingKeys 会更健壮。但这是可能的,并且对自定义密钥解码策略有一个很好的介绍。
首先,我们需要一个函数来进行 snake-case 转换。我真的很希望它在 stdlib 中公开,但事实并非如此,而且我不知道有什么方法可以 "get there" 而不只是复制代码。所以这里的代码直接基于JSONEncoder.swift。 (我什至讨厌将其复制到答案中,否则您将无法重现其余部分。)
// Makes me sad, but it's private to JSONEncoder.swift
// https://github.com/apple/swift/blob/master/stdlib/public/Darwin/Foundation/JSONEncoder.swift
func convertFromSnakeCase(_ stringKey: String) -> String {
guard !stringKey.isEmpty else { return stringKey }
// Find the first non-underscore character
guard let firstNonUnderscore = stringKey.firstIndex(where: { [=10=] != "_" }) else {
// Reached the end without finding an _
return stringKey
}
// Find the last non-underscore character
var lastNonUnderscore = stringKey.index(before: stringKey.endIndex)
while lastNonUnderscore > firstNonUnderscore && stringKey[lastNonUnderscore] == "_" {
stringKey.formIndex(before: &lastNonUnderscore)
}
let keyRange = firstNonUnderscore...lastNonUnderscore
let leadingUnderscoreRange = stringKey.startIndex..<firstNonUnderscore
let trailingUnderscoreRange = stringKey.index(after: lastNonUnderscore)..<stringKey.endIndex
var components = stringKey[keyRange].split(separator: "_")
let joinedString : String
if components.count == 1 {
// No underscores in key, leave the word as is - maybe already camel cased
joinedString = String(stringKey[keyRange])
} else {
joinedString = ([components[0].lowercased()] + components[1...].map { [=10=].capitalized }).joined()
}
// Do a cheap isEmpty check before creating and appending potentially empty strings
let result : String
if (leadingUnderscoreRange.isEmpty && trailingUnderscoreRange.isEmpty) {
result = joinedString
} else if (!leadingUnderscoreRange.isEmpty && !trailingUnderscoreRange.isEmpty) {
// Both leading and trailing underscores
result = String(stringKey[leadingUnderscoreRange]) + joinedString + String(stringKey[trailingUnderscoreRange])
} else if (!leadingUnderscoreRange.isEmpty) {
// Just leading
result = String(stringKey[leadingUnderscoreRange]) + joinedString
} else {
// Just trailing
result = joinedString + String(stringKey[trailingUnderscoreRange])
}
return result
}
我们还想要一把 CodingKey Swiss-Army 小刀,它也应该在 stdlib 中,但不是:
struct AnyKey: CodingKey {
var stringValue: String
var intValue: Int?
init?(stringValue: String) {
self.stringValue = stringValue
self.intValue = nil
}
init?(intValue: Int) {
self.stringValue = String(intValue)
self.intValue = intValue
}
}
这只是让您将任何字符串转换为 CodingKey。它来自 JSONDecoder docs.
最后,这就是所有样板垃圾。现在我们可以进入它的核心。没有办法直接说 "except for in Dictionaries" 。 CodingKeys 的解释独立于任何实际的 Decodable。所以你想要的是一个函数 "apply snake case unless this is a key nested inside of such-and-such key." 这是一个 returns 函数:
func convertFromSnakeCase(exceptWithin: [String]) -> ([CodingKey]) -> CodingKey {
return { keys in
let lastKey = keys.last!
let parents = keys.dropLast().compactMap {[=12=].stringValue}
if parents.contains(where: { exceptWithin.contains([=12=]) }) {
return lastKey
}
else {
return AnyKey(stringValue: convertFromSnakeCase(lastKey.stringValue))!
}
}
}
这样,我们只需要一个自定义密钥解码策略(注意,这使用 "userInfo" 的驼峰式版本,因为 CodingKey 路径是在应用转换之后):
decoder.keyDecodingStrategy = .custom(convertFromSnakeCase(exceptWithin: ["userInfo"]))
结果:
User(userName: "Mark", userInfo: ["b_a1234": "value_1", "c_d5678": "value_2"])
与仅添加 CodingKeys 相比,我不能保证这值得麻烦,但它是工具箱的有用工具。
或者,您可以使用 CodingKeys,这样您就拥有更多控制权,并且可以为每个字段指定名称。那么你不必设置keyDecodingStrategy
struct User: Decodable {
let userName: String
let userInfo: [String : String]
enum CodingKeys: String, CodingKey {
case userName = "user_name"
case userInfo = "user_info"
}
}
我有以下 JSON 对象:
{
"user_name":"Mark",
"user_info":{
"b_a1234":"value_1",
"c_d5678":"value_2"
}
}
我的 JSONDecoder
是这样设置的:
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
我的 Decodable
对象看起来像这样:
struct User: Decodable {
let userName: String
let userInfo: [String : String]
}
我面临的问题是 .convertFromSnakeCase
策略正在应用于字典的键,我希望这种情况不会发生。
// Expected Decoded userInfo
{
"b_a1234":"value_1",
"c_d5678":"value_2"
}
// Actual Decoded userInfo
{
"bA1234":"value_1",
"cD5678":"value_2"
}
我研究过使用自定义 keyDecodingStrategy
(但没有足够的信息来以不同方式处理字典),以及我的 Decodable
结构的自定义初始化程序(似乎键有此时已经转换)。
正确的处理方法是什么(仅为字典创建键转换异常)?
注意:我宁愿保留蛇形转换策略,因为我的实际 JSON 对象在蛇形中有很多属性。我当前的解决方法是使用 CodingKeys 枚举手动进行蛇形转换。
是的......但是,这有点棘手,最后可能只是添加 CodingKeys 会更健壮。但这是可能的,并且对自定义密钥解码策略有一个很好的介绍。
首先,我们需要一个函数来进行 snake-case 转换。我真的很希望它在 stdlib 中公开,但事实并非如此,而且我不知道有什么方法可以 "get there" 而不只是复制代码。所以这里的代码直接基于JSONEncoder.swift。 (我什至讨厌将其复制到答案中,否则您将无法重现其余部分。)
// Makes me sad, but it's private to JSONEncoder.swift
// https://github.com/apple/swift/blob/master/stdlib/public/Darwin/Foundation/JSONEncoder.swift
func convertFromSnakeCase(_ stringKey: String) -> String {
guard !stringKey.isEmpty else { return stringKey }
// Find the first non-underscore character
guard let firstNonUnderscore = stringKey.firstIndex(where: { [=10=] != "_" }) else {
// Reached the end without finding an _
return stringKey
}
// Find the last non-underscore character
var lastNonUnderscore = stringKey.index(before: stringKey.endIndex)
while lastNonUnderscore > firstNonUnderscore && stringKey[lastNonUnderscore] == "_" {
stringKey.formIndex(before: &lastNonUnderscore)
}
let keyRange = firstNonUnderscore...lastNonUnderscore
let leadingUnderscoreRange = stringKey.startIndex..<firstNonUnderscore
let trailingUnderscoreRange = stringKey.index(after: lastNonUnderscore)..<stringKey.endIndex
var components = stringKey[keyRange].split(separator: "_")
let joinedString : String
if components.count == 1 {
// No underscores in key, leave the word as is - maybe already camel cased
joinedString = String(stringKey[keyRange])
} else {
joinedString = ([components[0].lowercased()] + components[1...].map { [=10=].capitalized }).joined()
}
// Do a cheap isEmpty check before creating and appending potentially empty strings
let result : String
if (leadingUnderscoreRange.isEmpty && trailingUnderscoreRange.isEmpty) {
result = joinedString
} else if (!leadingUnderscoreRange.isEmpty && !trailingUnderscoreRange.isEmpty) {
// Both leading and trailing underscores
result = String(stringKey[leadingUnderscoreRange]) + joinedString + String(stringKey[trailingUnderscoreRange])
} else if (!leadingUnderscoreRange.isEmpty) {
// Just leading
result = String(stringKey[leadingUnderscoreRange]) + joinedString
} else {
// Just trailing
result = joinedString + String(stringKey[trailingUnderscoreRange])
}
return result
}
我们还想要一把 CodingKey Swiss-Army 小刀,它也应该在 stdlib 中,但不是:
struct AnyKey: CodingKey {
var stringValue: String
var intValue: Int?
init?(stringValue: String) {
self.stringValue = stringValue
self.intValue = nil
}
init?(intValue: Int) {
self.stringValue = String(intValue)
self.intValue = intValue
}
}
这只是让您将任何字符串转换为 CodingKey。它来自 JSONDecoder docs.
最后,这就是所有样板垃圾。现在我们可以进入它的核心。没有办法直接说 "except for in Dictionaries" 。 CodingKeys 的解释独立于任何实际的 Decodable。所以你想要的是一个函数 "apply snake case unless this is a key nested inside of such-and-such key." 这是一个 returns 函数:
func convertFromSnakeCase(exceptWithin: [String]) -> ([CodingKey]) -> CodingKey {
return { keys in
let lastKey = keys.last!
let parents = keys.dropLast().compactMap {[=12=].stringValue}
if parents.contains(where: { exceptWithin.contains([=12=]) }) {
return lastKey
}
else {
return AnyKey(stringValue: convertFromSnakeCase(lastKey.stringValue))!
}
}
}
这样,我们只需要一个自定义密钥解码策略(注意,这使用 "userInfo" 的驼峰式版本,因为 CodingKey 路径是在应用转换之后):
decoder.keyDecodingStrategy = .custom(convertFromSnakeCase(exceptWithin: ["userInfo"]))
结果:
User(userName: "Mark", userInfo: ["b_a1234": "value_1", "c_d5678": "value_2"])
与仅添加 CodingKeys 相比,我不能保证这值得麻烦,但它是工具箱的有用工具。
或者,您可以使用 CodingKeys,这样您就拥有更多控制权,并且可以为每个字段指定名称。那么你不必设置keyDecodingStrategy
struct User: Decodable {
let userName: String
let userInfo: [String : String]
enum CodingKeys: String, CodingKey {
case userName = "user_name"
case userInfo = "user_info"
}
}