Swift Codable 自定義屬性名稱

本文適合對Codable已經(jīng)懂得基本用法的人閱讀,如果你是使用Codable做模型轉(zhuǎn)換的話,
在屬性名由服務端人員或者第三方定的時候經(jīng)常會碰到一些問題,此處舉例兩個

  1. 參數(shù)名是蛇形命名法(Snake Case)而我們通用命名是駝峰命名(Camel Case)
  2. 如果接口返回的是以數(shù)字開頭的參數(shù)或者以iOS保留關鍵字作為參數(shù)。
    本文先上解決方案,再解釋原理

1. 蛇形命名轉(zhuǎn)駝峰命名規(guī)則

基本上我們都是使用JSONDecoder作為解析器,很多人不了解蘋果的具體轉(zhuǎn)換規(guī)則,我們先看下官方注釋

image.png

基本的原則就是\color{red}{首尾的下劃線保留},\color{red}{其他的下劃線去除},\color{red}{下劃線后面的首字母大寫其他小寫},但是實際使用的時候總是不如意,我們看一下實際的例子

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ù)的方法也可以留言相互交流

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容