Swift Codable 自定义属性名称

本文适合对Codable已经懂得基本用法的人阅读,如果你是使用Codable做模型转换的话,
在属性名由服务端人员或者第三方定的时候经常会碰到一些问题,此处举例两个

  1. 参数名是蛇形命名法(Snake Case)而我们通用命名是驼峰命名(Camel Case)
  2. 如果接口返回的是以数字开头的参数或者以iOS保留关键字作为参数。
    本文先上解决方案,再解释原理

1. 蛇形命名转驼峰命名规则

基本上我们都是使用JSONDecoder作为解析器,很多人不了解苹果的具体转换规则,我们先看下官方注释

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

打印出来的结果
Model(_1AB_: 1, _1ab2CdEf_: 0, abCd: "str")

特别是第二个参数 1ab2CdEf 转出的驼峰命名结果为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
/// 以_分割为数组
            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
        }

注释里写明了转换原则,我们根据上述的原则来重新查看刚转出来的几个参数

保留前后两个_ 其他的以_分割,比如 _1AB_2CD_ef_  ->  _[1AB, 2CD, ef]_
如果只有一个则直接返回 比如:_1AB_ -> _1AB_
如果有多个元素,第一个元素全部小写,第二个元素首字母大写: AB_CD    -> abCd
_1AB_2CD_ef_  -> _1ab2CdEf_,此处要注意一下,元素首字母大写,是首字母,不是首字符,所以2后面的C要大写其他小写

蛇形转驼峰命名介绍到此

2. Codable处理数字开头的参数

正常简单的方式如下:

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,列举出所有的参数对应的枚举,要转换的那个abCd 的rawValue 要等于接口返回的参数名,此处我指定了decoder.keyDecodingStrategy = .convertFromSnakeCase 所以rawValue是等于转换过的key也就是12abCd
以上做法有个弊病,就是如果参数名非常多或者说有多个参数都是以数字等不规范开头的,这做起来就麻烦了,下面介绍一种方式可以用于参考解决,同样先上解决方式再解释为何如此

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

此处使用自定义的参数转换,let key = keyArray.last 在取key.stringValue就是接口返回的参数名。已经取到的这个想怎么定参数规则,自然可以随意在block中定,这里是将接口返回的参数名先做一次驼峰命名转换再去除所有的数字,驼峰命名转换,直接使用系统方法,从源码里考出来的,上文有,可直接用,毕竟苹果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):
/// 自定义解析参数走这里
            self.container = Dictionary(container.map {
                /// 遍历container中的key,调用converter,也就是外面我们自己写的block
                /// 入参为decoder.codingPath是一个数组[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是一个结构体 struct _JSONKey : CodingKey,因为是个私有的结构体,我们自己也写一个_JSONKey,照源码抄出来
    var str = _convertFromSnakeCase(key!.stringValue)
    str = str.replacingOccurrences(of: "[0-9]", with: "", options: .regularExpression, range: str.startIndex..<str.endIndex)
    return _JSONKey(stringValue: str)! /// 返回参数需要CodingKey,所以我们自己也写一个_JSONKey
})

打印结果

Model(name: "halo", abCd: 0)

个人研究出的解决方式,有不妥的请留言指正,或者有更好的解决数字开头参数的方法也可以留言相互交流

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,699评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,124评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,127评论 0 358
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,342评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,356评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,057评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,654评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,572评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,095评论 1 318
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,205评论 3 339
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,343评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,015评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,704评论 3 332
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,196评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,320评论 1 271
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,690评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,348评论 2 358

推荐阅读更多精彩内容