本文適合對Codable已經(jīng)懂得基本用法的人閱讀,如果你是使用Codable做模型轉(zhuǎn)換的話,
在屬性名由服務端人員或者第三方定的時候經(jīng)常會碰到一些問題,此處舉例兩個
- 參數(shù)名是蛇形命名法(Snake Case)而我們通用命名是駝峰命名(Camel Case)
- 如果接口返回的是以數(shù)字開頭的參數(shù)或者以iOS保留關鍵字作為參數(shù)。
本文先上解決方案,再解釋原理
1. 蛇形命名轉(zhuǎn)駝峰命名規(guī)則
基本上我們都是使用JSONDecoder作為解析器,很多人不了解蘋果的具體轉(zhuǎn)換規(guī)則,我們先看下官方注釋

基本的原則就是
struct Model: Codable {
var _1AB_: Int
var _1ab2CdEf_: Int
var abCd: String
}
let json = """
{"_1AB_": 1, "_1AB_2CD_ef_": 0, "AB_CD": "str"}
"""
let jsonData = json.data(using: .utf8)
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
var result: Model!
if let data = jsonData {
result = try! decoder.decode(Model.self, from: data)
print(result!)
}
打印出來的結(jié)果
Model(_1AB_: 1, _1ab2CdEf_: 0, abCd: "str")
特別是第二個參數(shù) 1ab2CdEf 轉(zhuǎn)出的駝峰命名結(jié)果為1ab2CdEf,這就讓人很不理解了,但是如果查看源碼就能很清晰明了,swift 中查看Foundation里JSONDecoder源碼
fileprivate static func _convertFromSnakeCase(_ stringKey: String) -> String {
guard !stringKey.isEmpty else { return stringKey }
// Find the first non-underscore character
/// 找到 第一個非_字符的位置
guard let firstNonUnderscore = stringKey.firstIndex(where: { $0 != "_" }) 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
/// 以_分割為數(shù)組
let 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 { $0.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
}
注釋里寫明了轉(zhuǎn)換原則,我們根據(jù)上述的原則來重新查看剛轉(zhuǎn)出來的幾個參數(shù)
保留前后兩個_ 其他的以_分割,比如 _1AB_2CD_ef_ -> _[1AB, 2CD, ef]_
如果只有一個則直接返回 比如:_1AB_ -> _1AB_
如果有多個元素,第一個元素全部小寫,第二個元素首字母大寫: AB_CD -> abCd
_1AB_2CD_ef_ -> _1ab2CdEf_,此處要注意一下,元素首字母大寫,是首字母,不是首字符,所以2后面的C要大寫其他小寫
蛇形轉(zhuǎn)駝峰命名介紹到此
2. Codable處理數(shù)字開頭的參數(shù)
正常簡單的方式如下:
struct Model: Codable {
var name: String
var abCd: Int
enum CodingKeys: String, CodingKey {
case name
case abCd = "12abCd"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
self.abCd = try container.decode(Int.self, forKey: .abCd)
}
}
let json = """
{"name": "halo", "12AB_CD": 0}
"""
let jsonData = json.data(using: .utf8)
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
var result: Model!
if let data = jsonData {
result = try! decoder.decode(Model.self, from: data)
print(result!)
}
做法是自定義CodingKeys,列舉出所有的參數(shù)對應的枚舉,要轉(zhuǎn)換的那個abCd 的rawValue 要等于接口返回的參數(shù)名,此處我指定了decoder.keyDecodingStrategy = .convertFromSnakeCase 所以rawValue是等于轉(zhuǎn)換過的key也就是12abCd
以上做法有個弊病,就是如果參數(shù)名非常多或者說有多個參數(shù)都是以數(shù)字等不規(guī)范開頭的,這做起來就麻煩了,下面介紹一種方式可以用于參考解決,同樣先上解決方式再解釋為何如此
struct _JSONKey : CodingKey {
public var stringValue: String
public var intValue: Int?
init?(stringValue: String) {
self.stringValue = stringValue
}
init?(intValue: Int) {
self.intValue = intValue
self.stringValue = "\(intValue)"
}
}
struct Model: Codable {
var name: String
var abCd: Int
}
let json = """
{"name": "halo", "12AB_CD": 0}
"""
let jsonData = json.data(using: .utf8)
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .custom({ keyArray in
let key = keyArray.last
var str = _convertFromSnakeCase(key!.stringValue)
str = str.replacingOccurrences(of: "[0-9]", with: "", options: .regularExpression, range: str.startIndex..<str.endIndex)
return _JSONKey(stringValue: str)!
})
var result: Model!
if let data = jsonData {
result = try! decoder.decode(Model.self, from: data)
print(result!)
}
此處使用自定義的參數(shù)轉(zhuǎn)換,let key = keyArray.last 在取key.stringValue就是接口返回的參數(shù)名。已經(jīng)取到的這個想怎么定參數(shù)規(guī)則,自然可以隨意在block中定,這里是將接口返回的參數(shù)名先做一次駝峰命名轉(zhuǎn)換再去除所有的數(shù)字,駝峰命名轉(zhuǎn)換,直接使用系統(tǒng)方法,從源碼里考出來的,上文有,可直接用,畢竟蘋果Foudation源碼也是用swift寫的。
源碼也非常簡單,冗雜代碼不解析,直指核心
/// Initializes `self` by referencing the given decoder and container.
init(referencing decoder: __JSONDecoder, wrapping container: [String : Any]) {
self.decoder = decoder
switch decoder.options.keyDecodingStrategy {
case .useDefaultKeys:
self.container = container
case .convertFromSnakeCase:
// Convert the snake case keys in the container to camel case.
// If we hit a duplicate key after conversion, then we'll use the first one we saw. Effectively an undefined behavior with JSON dictionaries.
self.container = Dictionary(container.map {
key, value in (JSONDecoder.KeyDecodingStrategy._convertFromSnakeCase(key), value)
}, uniquingKeysWith: { (first, _) in first })
case .custom(let converter):
/// 自定義解析參數(shù)走這里
self.container = Dictionary(container.map {
/// 遍歷container中的key,調(diào)用converter,也就是外面我們自己寫的block
/// 入?yún)閐ecoder.codingPath是一個數(shù)組[CodingKey],加入最新的一個key對應的CodingKey
key, value in (converter(decoder.codingPath + [_JSONKey(stringValue: key, intValue: nil)]).stringValue, value)
}, uniquingKeysWith: { (first, _) in first })
}
self.codingPath = decoder.codingPath
}
核心代碼就上面一段,我們重新解析一下我們的block
decoder.keyDecodingStrategy = .custom({ keyArray in
let key = keyArray.last // 最后一個就是生面寫的 [_JSONKey(stringValue: key, intValue: nil)]) 是一個CodingKey, _JSONKey是一個結(jié)構(gòu)體 struct _JSONKey : CodingKey,因為是個私有的結(jié)構(gòu)體,我們自己也寫一個_JSONKey,照源碼抄出來
var str = _convertFromSnakeCase(key!.stringValue)
str = str.replacingOccurrences(of: "[0-9]", with: "", options: .regularExpression, range: str.startIndex..<str.endIndex)
return _JSONKey(stringValue: str)! /// 返回參數(shù)需要CodingKey,所以我們自己也寫一個_JSONKey
})
打印結(jié)果
Model(name: "halo", abCd: 0)
個人研究出的解決方式,有不妥的請留言指正,或者有更好的解決數(shù)字開頭參數(shù)的方法也可以留言相互交流