美文网首页
Swift Codable 自定义属性名称

Swift Codable 自定义属性名称

作者: 佐佑卟份 | 来源:发表于2021-09-18 16:07 被阅读0次

本文适合对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)

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

相关文章

网友评论

      本文标题:Swift Codable 自定义属性名称

      本文链接:https://www.haomeiwen.com/subject/yaxygltx.html