当属性不在 json 字符串中时,属性包装器会导致 JSONDecoder 失败

A property wrapper causes JSONDecoder to fail when property is not in the json string

提问人:Mando 提问时间:8/15/2023 更新时间:8/15/2023 访问量:53

问:

我有一个数据模型类,其中包含从 json 反序列化的自定义规则。具体来说,该属性可以在 JSON 中定义为 或 。在我的模型类中,它应该始终是 .我已经实现了一个支持自定义反序列化规则:flexiblePropString?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?regularPropflexiblePropertyJSONDecoder

iOS JSON Swift JsonDecoder 属性包装器

评论


答:

3赞 Itai Ferber 8/15/2023 #1

(注意:这个答案改编自我在 Swift 论坛上的一篇类似帖子,解释了这种行为。

遗憾的是,属性包装器无法影响合成,从而允许完全省略特定键的值。Codable

当你写

@FlexibleInt varflexibleProp: Int?

编译器生成等效的

private var _varflexibleProp: FlexibleInt
var varflexibleProp: Int? {
    get { return _varflexibleProp.wrappedValue }
    set { _varflexibleProp.wrappedValue = newValue }
}

因为是“real”属性(并且只是一个计算属性),所以为了编码和解码,编译器会进行编码和解码。这一点至关重要,因为它允许以自己的一致性截获属性的编码和解码。_varflexiblePropvarflexibleProp_varflexiblePropFlexibleIntCodable

但需要注意的是,类型是 ,它不是 — 它只是包含它的内部。这很重要,因为值对于综合是特殊的:_varflexiblePropFlexibleIntOptionalOptionalOptionalCodable

  • 当编译器遇到属性时,它会生成对 encodeIfPresent(_:forKey:)/decodeIfPresent(_:forKey:) 的调用,如果是,则完全跳过对值的编码,并且至关重要的是,如果键在键控容器中不存在,则完全跳过对值的解码Optionalnil
  • 当编译器遇到非属性时,它会生成对 encode(_:forKey:)/decode(_:forKey: 的调用,即使 ,它也会无条件地对值进行编码,并且无条件地要求键在解码时存在Optionalnil

最后一点是有问题的:因为 is not ,编译器尝试将其初始化为_varflexiblePropOptional

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 论坛线程中,属性包装器作者没有给它一个零参数初始值设定项,而是有一个单参数初始值设定项,它采用了 ;这很快就会演变成如何初始化这个非属性的猜谜游戏,这对编译器来说可能根本不安全。_varflexiblePropFlexibleInt()wrappedValueOptional


归根结底,此行为与具体无关,而是与编译器如何合成一致性有关。现在的解决方法是直接实现,使用自己并使用默认值进行初始化。JSONDecoderCodablePersonInfo.init(from:)decodeIfPresent(_:forKey:)_varflexibleProp

关于将来如何处理这个问题,线程中还有更多讨论,所以如果你好奇的话,值得一读。(而且,现在宏即将出现在 Swift 5.9 中,将来可能会像现在这样从编译器中提取一致性,而是作为宏实现——如果是这样,可以添加切换以更好地影响它如何发生。Codable

评论

0赞 Mando 8/18/2023
是否可以像以前一样解码所有其他属性并仅指定此给定属性?或者我是否需要通过指定每个属性的名称和类型来解码每个属性?decodeIfPresentflexible
1赞 Itai Ferber 8/18/2023
@Mando 不,不幸的是:如果你指定了自己的 OR 方法,用于合成一致性的编译器机制根本不会启动,所以你必须完整地实现该方法,包括你想要的所有属性。init(from:)encode(to:)