decoder.container(keyedBy:) 抛出 DecodingError.typeMismatch 错误。可编码错误?

decoder.container(keyedBy:) throws DecodingError.typeMismatch error. Codable bug?

我正在使用 Codable 协议从 Web API 解码 JSON。这个 API 我的 Swift 数据模型包括 class 继承(subclasses)和组合(对象作为其他对象的属性)。在 JSON 中,相同的 属性 名称可能表示一个完整的对象,或者表示该对象在数据库中的 ID 的单个字符串。

据我所知,使用 Codable 处理这种 JSON 的唯一模式是在对象的初始值设定项 init(from decoder: Decoder) 中进行解码 "manually",然后首先尝试解码整个对象。如果失败(通过抛出必须捕获的错误),则重试将 属性 解码为 String.

只要包含变体 属性 的对象不是另一个 Decodable class 的子class,它就可以很好地工作。在这种情况下,解码基础 class 的属性将在调用函数 decoder.container(keyedBy:).

时抛出错误 DecodingError.typeMismatch

请参阅下面的示例代码。

这是一个已知错误吗? And/or 在这种情况下我是否缺少另一种解码方法?

顺便说一句,如果在抛出 DecodingError.typeMismatch 错误后调用 decoder.container(keyedBy:),即使该错误已被捕获,也会在单个函数中抛出相同的错误。

import Foundation

// A `Base` class
class Base: Codable {
    var baseProperty: String? = "baseProperty"
    init() {}

    private enum CodingKeys: String, CodingKey { case baseProperty }

    required init(from decoder: Decoder) throws {
//===>> The next line will throw DecodingError.typeMismatch
        let container = try decoder.container(keyedBy: CodingKeys.self)
        baseProperty = try container.decode(String.self, forKey: .baseProperty)
    }
}

// A Subclass of `Base`
class Sub: Base {
    // An `Other` class which is a property of the `Sub` class
    class Other: Codable { var id: String? = "otherID" }
    var subProperty: Other? = nil
    override init() { super.init() }

    private enum CodingKeys: String, CodingKey { case subProperty }

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        do { subProperty = try container.decode(Other.self,
                                                forKey: .subProperty)
        }
        catch { // We didn't find a whole `Other` object in the JSON; look for a `String` instead
            let s = try container.decode(String.self, forKey: .subProperty)
            subProperty = Other()
            subProperty?.id = s
        }
        try super.init(from: decoder)
    }
}

// Some sample JSON data:
let json = """
{"baseProperty" : "baseProperty",
 "subProperty" : "someIDString"}
""".data(using: .utf8)!

// MAIN program -----------------------------------------------------
// Decode the JSON to produce a new Sub class instance
do {
    _ = try JSONDecoder().decode(Sub.self, from: json)
}
catch DecodingError.typeMismatch( _, let context) {
    print("DecodingError.typeMismatch: \(context.debugDescription)")
    print("DecodingError.Context: codingPath:")
    for i in 0..<context.codingPath.count { print("  [\(i)] = \(context.codingPath[i])") }
}

这个is a known bug, which has been fixed in this pull request(并且会变成Swift 4.1)。

问题基本上是当 Other 的解码失败时,解码器将忘记从其内部堆栈中弹出其容器,因此意味着任何未来的解码 start.subProperty 的嵌套容器中;因此,为什么在尝试从那里解码对象时会出现类型不匹配错误(因为只有一个字符串!)。

在修复之前,一种解决方法是不使用 decode(_:forKey:);获取 超级解码器 ,然后尝试从中解码 Other

所以替换这个:

subProperty = try container.decode(Other.self, forKey: .subProperty)

有了这个:

let subPropertyDecoder = try container.superDecoder(forKey: .subProperty)
subProperty = try Other(from: subPropertyDecoder)

这是可行的,因为现在我们有了一个全新的解码器实例,我们不能破坏主解码器的堆栈。