提问人:Mando 提问时间:8/15/2023 更新时间:8/15/2023 访问量:53
当属性不在 json 字符串中时,属性包装器会导致 JSONDecoder 失败
A property wrapper causes JSONDecoder to fail when property is not in the json string
问:
我有一个数据模型类,其中包含从 json 反序列化的自定义规则。具体来说,该属性可以在 JSON 中定义为 或 。在我的模型类中,它应该始终是 .我已经实现了一个支持自定义反序列化规则:flexibleProp
String?
Int?
Int?
propertyWrapper
class PersonInfo: Codable {
var regularProp: String?
@FlexibleInt varflexibleProp: Int?
init() {
}
}
@propertyWrapper struct FlexibleInt: Codable {
var wrappedValue: Int?
init() {
self.wrappedValue= nil
}
init(fromdecoder: Decoder) throws{
letcontainer = trydecoder.singleValueContainer()
ifletvalue = try? container.decode(String.self) {
wrappedValue= Int(value)
} elseifletintValue = try? container.decode(Int.self) {
wrappedValue= intValue
} else{
wrappedValue= nil
}
}
func encode(toencoder: Encoder) throws {
varcontainer = encoder.singleValueContainer()
trycontainer.encode(wrappedValue)
}
}
let decoder = JSONDecoder()
let payload1 = "{ \"flexibleProp\": \"123888\", \"regularProp\": \"qqq\" }"
letperson1= trydecoder.decode(PersonInfo.self, from: payload1.data(using: .utf8)!)
let payload2 = "{ \"flexibleProp\": \"\" }"
letperson2= trydecoder.decode(PersonInfo.self, from: payload2.data(using: .utf8)!)
let payload3 = "{ \"flexibleProp\": \"sss\" }"
letperson3= trydecoder.decode(PersonInfo.self, from: payload3.data(using: .utf8)!)
let payload4 = "{ }"
letperson4= trydecoder.decode(PersonInfo.self, from: payload4.data(using: .utf8)!) // FAILS HERE <------------
它在大多数情况下都能正常工作,只有最后一个测试用例失败,当 json 字符串中完全缺少属性(未定义键和值)时:flexibleProp
▿ DecodingError
▿ keyNotFound : 2 elements
- .0 : CodingKeys(stringValue: "flexibleProp", intValue: nil)
▿ .1 : Context
- codingPath : 0 elements
- debugDescription : "No value associated with key CodingKeys(stringValue: \"flexibleProp\", intValue: nil) (\"flexibleProp\")."
- underlyingError : nil
我对其他属性没有问题,当 json 中缺少时,它在结果中保持为零,但对于它无法反序列化整个对象,而不仅仅是保持属性为零。当该属性未在 json 字符串中定义时,正确处理该属性的最佳方法是什么?String?
regularProp
flexibleProperty
JSONDecoder
答:
(注意:这个答案改编自我在 Swift 论坛上的一篇类似帖子,解释了这种行为。
遗憾的是,属性包装器无法影响合成,从而允许完全省略特定键的值。Codable
当你写
@FlexibleInt varflexibleProp: Int?
编译器生成等效的
private var _varflexibleProp: FlexibleInt
var varflexibleProp: Int? {
get { return _varflexibleProp.wrappedValue }
set { _varflexibleProp.wrappedValue = newValue }
}
因为是“real”属性(并且只是一个计算属性),所以为了编码和解码,编译器会进行编码和解码。这一点至关重要,因为它允许以自己的一致性截获属性的编码和解码。_varflexibleProp
varflexibleProp
_varflexibleProp
FlexibleInt
Codable
但需要注意的是,类型是 ,它不是 — 它只是包含它的内部。这很重要,因为值对于综合是特殊的:_varflexibleProp
FlexibleInt
Optional
Optional
Optional
Codable
- 当编译器遇到属性时,它会生成对 encodeIfPresent(_:forKey:)/
decodeIfPresent(_:forKey:)
的调用,如果是,则完全跳过对值的编码,并且至关重要的是,如果键在键控容器中不存在,则完全跳过对值的解码Optional
nil
- 当编译器遇到非属性时,它会生成对 encode(_:forKey:
)/decode(_:forKey:
)
的调用,即使 ,它也会无条件地对值进行编码,并且无条件地要求键在解码时存在Optional
nil
最后一点是有问题的:因为 is not ,编译器尝试将其初始化为_varflexibleProp
Optional
init(from decoder: Decoder) throws {
let container = try container(keyedBy: CodingKeys.self)
_varflexibleProp = try container.decode(FlexibleInt.self, forKey: .varflexibleProp)
// ...
}
decode(_:forKey:)
在解码任何东西之前,必须检查密钥是否存在,因此,在您的情况下,在调用之前会抛出错误——因为甚至没有任何数据可供您解码。FlexibleInt.init(from:)
如果编译器确实尝试使用 ,它必须为 提供某种默认值,这并不总是显而易见的:decodeIfPresent(_:forKey:)
_varflexibleProp
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
// `varflexibleProp` is a computed property, which can't be assigned to in an initializer.
// You have to write to `_varflexibleProp`, which is the real property.
_varflexibleProp = try container.decodeIfPresent(FlexibleInt.self, forKey: .varflexibleProp)
// ❌ error: Value of optional type 'FlexibleInt?' must be unwrapped to a value of type 'FlexibleInt'
// ^ oops! what value do we assign to _varflexibleProp?
}
在这里,似乎很明显应该为其分配一个值 ,但编译器无法知道它是否具有您希望它具有的语义(例如,如果它有副作用怎么办?在链接的 Swift 论坛线程中,属性包装器作者没有给它一个零参数初始值设定项,而是有一个单参数初始值设定项,它采用了 ;这很快就会演变成如何初始化这个非属性的猜谜游戏,这对编译器来说可能根本不安全。_varflexibleProp
FlexibleInt()
wrappedValue
Optional
归根结底,此行为与具体无关,而是与编译器如何合成一致性有关。现在的解决方法是直接实现,使用自己并使用默认值进行初始化。JSONDecoder
Codable
PersonInfo.init(from:)
decodeIfPresent(_:forKey:)
_varflexibleProp
关于将来如何处理这个问题,线程中还有更多讨论,所以如果你好奇的话,值得一读。(而且,现在宏即将出现在 Swift 5.9 中,将来可能会像现在这样从编译器中提取一致性,而是作为宏实现——如果是这样,可以添加切换以更好地影响它如何发生。Codable
评论
decodeIfPresent
flexible
init(from:)
encode(to:)
评论