Swift - Codable 解码设置默认值
上一篇 Swift - Codable 使用小记 文章中介绍了 Codable 的使用,它能够把 JSON 数据转换成 Swift 代码中使用的类型。本文来进一步研究使用 Codable 解码如何设置默认值的问题。
解码遇到的问题 之前的文章中提到了,遇到 JSON 数据中字段为空的情况,把属性设置为可选的,当返回为空对象或 null 时,解析为 nil。 当我们希望字段为空时,对应的属性要设置一个默认值,我们处理的一种方法是重写 init(from decoder: Decoder) 方法,在 decodeIfPresent 判断设置默认值,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 struct Person: Decodable { let name: String let age: Int enum CodingKeys: String, CodingKey { case name, age } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) name = try container.decode(String.self, forKey: .name) age = try container.decodeIfPresent(Int.self, forKey: .age) ?? -1 } } let data = """ { "name": "小明", "age": null} """ let p = try JSONDecoder().decode(Person.self, from: data.data(using: .utf8)!) //Person(name: "小明", age: -1)
这种方法显然很麻烦,需要为每个类型添加 CodingKeys 和 init(from decoder: Decoder) 代码,有没有更好、更方便的方法呢? 我们先来了解一下 property wrapper 。
Property Wrapper property wrapper 属性包装器,在管理属性如何存储和定义属性的代码之间添加了一层隔离。当使用属性包装器时,你只需在定义属性包装器时编写一次管理代码,然后应用到多个属性上来进行复用。它相当于提供一个特殊的盒子,把属性值包装进去。当你把一个包装器应用到一个属性上时,编译器将合成提供包装器存储空间和通过包装器访问属性的代码。
例如有个需求,要求属性值不得大于某个数,实现的时候要一个个在属性 set 方法中判断是否大于,然后进行处理,这样很显然很麻烦。这时就可以定义一个属性包装器,在这里进行处理,然后把包装器应用到属性上去,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 @propertyWrapper struct SmallNumber { private var maximum: Int private var number: Int var wrappedValue: Int { get { return number } set { number = min(newValue, maximum) } } init() { maximum = 12 number = 0 } init(wrappedValue: Int) { maximum = 12 number = min(wrappedValue, maximum) } init(wrappedValue: Int, maximum: Int) { self.maximum = maximum number = min(wrappedValue, maximum) } } struct SmallRectangle { @SmallNumber var height: Int @SmallNumber(wrappedValue: 10, maximum: 20) var width: Int } var rect = SmallRectangle() print(rect.height, rect.width) //0 10 rect.height = 30 print(rect.height) //12 rect.width = 40 print(rect.width) //20 print(rect) //SmallRectangle(_height: SmallNumber(maximum: 12, number: 12), _width: SmallNumber(maximum: 20, number: 20))
上面例子中 SmallNumber 定义了三个构造器,可使用构造器来设置被包装值和最大值, height 不大于 12,width 不大于 20。 通过打印的内容可看到 _height: SmallNumber(maximum: 12, number: 12),被 SmallNumber 声明的属性,实际上存储的类型是 SmallNumber 类型,只不过编译器进行了处理,对外暴露的类型依然是原来的类型 Int。 编译器对属性的处理,相当于下面的代码处理方法:
1 2 3 4 5 6 7 8 struct SmallRectangle { private var _height = SmallNumber() var height: Int { get { return _height.wrappedValue } set { _height.wrappedValue = newValue } } //... }
将属性 height 包装在 SmallNumber 结构体中,get set 操作的值其实是结构体中 wrappedValue 的值。 弄清楚这些之后,我们利用属性包装器给属性包装一层,在 Codable 解码的时候操作的是 wrappedValue ,这时我们就可以在属性包装器中进行判断,设置默认值。顺着这个思路下面我们来实现以下。
设置默认值 通过前面的分析,大概有了思路,定义一个能够提供默认值的 Default property wrapper ,利用这个 Default 来包装属性,Codable 解码的时候把值赋值 Default 的 wrappedValue,如解码失败就在这里设置默认值。
初步实现 初步实现的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @propertyWrapper struct Default: Decodable { var wrappedValue: Int init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() wrappedValue = (try? container.decode(Int.self)) ?? -1 } } struct Person: Decodable { @Default var age: Int } let data = #"{ "age": null}"# let p = try JSONDecoder().decode(Person.self, from: data.data(using: .utf8)!) print(p, p.age) //Person(_age: Default(wrappedValue: -1)) -1
可以看到上面的例子中,JSON 数据为 null,解码到 age 设置了默认值 -1。
改进代码 接着我们来改进一下,上面例子只是对 Int 类型的设置了默认值,下面来使用泛型,扩展一下对别的类型支持。 还有一个问题就是,如果 JSON 中 age 这个 key 缺失的情况下,依然会发生错误,因为我们所使用的解码器默认生成的代码是要求 key 存在的。需要改进一下为 container 重写对于 Default 类型解码的实现。 改进后的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 protocol DefaultValue { associatedtype Value: Decodable static var defaultValue: Value { get } } @propertyWrapper struct Default<T: DefaultValue> { var wrappedValue: T.Value } extension Default: Decodable { init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() wrappedValue = (try? container.decode(T.Value.self)) ?? T.defaultValue } } extension KeyedDecodingContainer { func decode<T>(_ type: Default<T>.Type, forKey key: Key) throws -> Default<T> where T: DefaultValue { //判断 key 缺失的情况,提供默认值 (try decodeIfPresent(type, forKey: key)) ?? Default(wrappedValue: T.defaultValue) } } extension Int: DefaultValue { static var defaultValue = -1 } extension String: DefaultValue { static var defaultValue = "unknown" } struct Person: Decodable { @Default<String> var name: String @Default<Int> var age: Int } let data = #"{ "name": null, "age": null}"# let p = try JSONDecoder().decode(Person.self, from: data.data(using: .utf8)!) print(p, p.name, p.age) //Person(_name: Default<Swift.String>(wrappedValue: "unknown"), _age: Default<Swift.Int>(wrappedValue: -1)) //unknown -1
这样如我们需要对某种类型在解码时设置默认值,我们只需要对应的添加个扩展,遵循 DefaultValue 协议,提供一个想要的默认值 defaultValue 即可。 而且对于 JSON 中 key 缺失的情况,也做了处理,重写了 container.decode() 方法,判断 key 缺失的情况,如 key 缺失,返回默认值。
设置多种默认值的情况 有时我们再不同情况下,同种类型的数据需要设置不同的默认值,例如 String 类型的属性,在有的地方默认值需要设置为 “unknown”,有的地方则需要设置为 “unnamed”,这是我们处理方法如下:
1 2 3 4 5 6 7 8 9 10 11 extension String { struct Unknown: DefaultValue { static var defaultValue = "unknown" } struct Unnamed: DefaultValue { static var defaultValue = "unnamed" } } @Default<String.Unnamed> var name: String @Default<String.Unknown> var text: String
这样就实现了不同的情况定义不同的默认值。
其他问题 还有一个问题,自定义的数据类型,解码到异常的数据可能导致我们的代码崩溃,还是举之前文章中的例子,枚举类型解析,如下:
1 2 3 4 5 6 7 8 enum Gender: String, Codable { case male case female } struct Person: Decodable { var gender: Gender } //{ "gender": "other" }
当 JSON 数据中的 gender 对应的值不在 Gender 枚举的 case 字段中,解码的时候会出现异常,即使 gender 属性是可选的,也会出现异常。要解决这个问题,也可以重写 init(from decoder: Decoder) ,在里面进行判断是否解码异常,然后进行处理。
相比于使用枚举,其实这里用一个带有 raw value 的 struct 来表示会更好,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 struct Gender: RawRepresentable, Codable { static let male = Gender(rawValue: "male") static let female = Gender(rawValue: "female") let rawValue: String } struct XMan: Decodable { var gender: Gender } let mData = #"{ "gender": "other" }"# let m = try JSONDecoder().decode(XMan.self, from: mData.data(using: .utf8)!) print(m) //XMan(gender: Gender(rawValue: "other")) print(m.gender == .male) //false
这样,就算以后为 Gender 添加了新的字符串,现有的实现也不会被破坏,这样也更加稳定。
References https://onevcat.com/2020/11/codable-default/ https://docs.swift.org/swift-book/LanguageGuide/Properties.html#ID617 http://marksands.github.io/2019/10/21/better-codable-through-property-wrappers.html