如果单元素解码失败,Swift JSONDecode 解码数组将失败

Posted

技术标签:

【中文标题】如果单元素解码失败,Swift JSONDecode 解码数组将失败【英文标题】:Swift JSONDecode decoding arrays fails if single element decoding fails 【发布时间】:2018-03-02 20:48:39 【问题描述】:

在使用 Swift4 和 Codable 协议时,我遇到了以下问题 - 似乎没有办法允许 JSONDecoder 跳过数组中的元素。 例如,我有以下 JSON:

[
    
        "name": "Banana",
        "points": 200,
        "description": "A banana grown in Ecuador."
    ,
    
        "name": "Orange"
    
]

还有一个 Codable 结构:

struct GroceryProduct: Codable 
    var name: String
    var points: Int
    var description: String?

解码这个json时

let decoder = JSONDecoder()
let products = try decoder.decode([GroceryProduct].self, from: json)

结果products 为空。这是意料之中的,因为 JSON 中的第二个对象没有 "points" 键,而 pointsGroceryProduct 结构中不是可选的。

问题是如何允许JSONDecoder“跳过”无效对象?

【问题讨论】:

我们无法跳过无效对象,但如果它为 nil,您可以指定默认值。 为什么不能将points 声明为可选? 【参考方案1】:

不幸的是,Swift 4 API 没有 init(from: Decoder) 的可失败初始化程序。

我看到的只有一种解决方案是实现自定义解码,为可选字段提供默认值,并为所需数据提供可能的过滤器:

struct GroceryProduct: Codable 
    let name: String
    let points: Int?
    let description: String

    private enum CodingKeys: String, CodingKey 
        case name, points, description
    

    init(from decoder: Decoder) throws 
        let container = try decoder.container(keyedBy: CodingKeys.self)
        name = try container.decode(String.self, forKey: .name)
        points = try? container.decode(Int.self, forKey: .points)
        description = (try? container.decode(String.self, forKey: .description)) ?? "No description"
    


// for test
let dict = [["name": "Banana", "points": 100], ["name": "Nut", "description": "Woof"]]
if let data = try? JSONSerialization.data(withJSONObject: dict, options: []) 
    let decoder = JSONDecoder()
    let result = try? decoder.decode([GroceryProduct].self, from: data)
    print("rawResult: \(result)")

    let clearedResult = result?.filter  $0.points != nil 
    print("clearedResult: \(clearedResult)")

【讨论】:

【参考方案2】:

有两种选择:

    将结构的所有成员声明为可缺少键的可选成员

    struct GroceryProduct: Codable 
        var name: String
        var points : Int?
        var description: String?
    
    

    编写自定义初始化程序以在 nil 情况下分配默认值。

    struct GroceryProduct: Codable 
        var name: String
        var points : Int
        var description: String
    
        init(from decoder: Decoder) throws 
            let values = try decoder.container(keyedBy: CodingKeys.self)
            name = try values.decode(String.self, forKey: .name)
            points = try values.decodeIfPresent(Int.self, forKey: .points) ?? 0
            description = try values.decodeIfPresent(String.self, forKey: .description) ?? ""
        
    
    

【讨论】:

第二个选项中最好使用trydecodeIfPresent,而不是try?decode。我们需要在没有key的情况下设置默认值,而不是在任何解码失败的情况下,比如key存在,但是类型错误。 嘿@vadian 你知道任何其他涉及自定义初始化程序以在类型不匹配的情况下分配默认值的问题吗?我有一个键,它是一个 Int 但有时会是 JSON 中的一个字符串,所以我尝试用 deviceName = try values.decodeIfPresent(Int.self, forKey: .deviceName) ?? 00000 做你上面所说的,所以如果它失败它只会输入 0000 但它仍然失败。 在这种情况下 decodeIfPresent 是错误的 API 因为密钥确实存在。使用另一个 do - catch 块。解码String,如果出现错误,解码Int【参考方案3】:

一种选择是使用尝试解码给定值的包装器类型;如果不成功,存储nil

struct FailableDecodable<Base : Decodable> : Decodable 

    let base: Base?

    init(from decoder: Decoder) throws 
        let container = try decoder.singleValueContainer()
        self.base = try? container.decode(Base.self)
    

然后我们可以解码这些数组,将您的 GroceryProduct 填充到 Base 占位符:

import Foundation

let json = """
[
    
        "name": "Banana",
        "points": 200,
        "description": "A banana grown in Ecuador."
    ,
    
        "name": "Orange"
    
]
""".data(using: .utf8)!


struct GroceryProduct : Codable 
    var name: String
    var points: Int
    var description: String?


let products = try JSONDecoder()
    .decode([FailableDecodable<GroceryProduct>].self, from: json)
    .compactMap  $0.base  // .flatMap in Swift 4.0

print(products)

// [
//    GroceryProduct(
//      name: "Banana", points: 200,
//      description: Optional("A banana grown in Ecuador.")
//    )
// ]

然后我们使用.compactMap $0.base 过滤掉nil 元素(那些在解码时引发错误的元素)。

这将创建一个[FailableDecodable&lt;GroceryProduct&gt;] 的中间数组,这应该不是问题;但是,如果您想避免这种情况,您总是可以创建另一种包装器类型来解码和解包来自未键入容器的每个元素:

struct FailableCodableArray<Element : Codable> : Codable 

    var elements: [Element]

    init(from decoder: Decoder) throws 

        var container = try decoder.unkeyedContainer()

        var elements = [Element]()
        if let count = container.count 
            elements.reserveCapacity(count)
        

        while !container.isAtEnd 
            if let element = try container
                .decode(FailableDecodable<Element>.self).base 

                elements.append(element)
            
        

        self.elements = elements
    

    func encode(to encoder: Encoder) throws 
        var container = encoder.singleValueContainer()
        try container.encode(elements)
    

然后您将解码为:

let products = try JSONDecoder()
    .decode(FailableCodableArray<GroceryProduct>.self, from: json)
    .elements

print(products)

// [
//    GroceryProduct(
//      name: "Banana", points: 200,
//      description: Optional("A banana grown in Ecuador.")
//    )
// ]

【讨论】:

如果基础对象不是一个数组,但它包含一个数组怎么办?像 "products": ["name": "banana"...,...] @ludvigeriksson 您只想在该结构中执行解码,例如:gist.github.com/hamishknight/c6d270f7298e4db9e787aecb5b98bcae Swift 的 Codable 很简单,直到现在.. 不能再简单一点吗? @Hamish 我没有看到此行的任何错误处理。如果这里抛出错误会发生什么var container = try decoder.unkeyedContainer() @bibscy 它在init(from:) throws 的主体内,因此Swift 会自动将错误传播回调用者(在这种情况下,解码器会将错误传播回JSONDecoder.decode(_:from:) 调用)。 【参考方案4】:

问题在于,当迭代容器时,container.currentIndex 不会递增,因此您可以尝试使用不同的类型再次解码。

因为 currentIndex 是只读的,所以一个解决方案是自己增加它,成功解码一个虚拟对象。我采用了@Hamish 解决方案,并使用自定义初始化编写了一个包装器。

这个问题是当前的 Swift 错误:https://bugs.swift.org/browse/SR-5953

此处发布的解决方案是其中一个 cmets 中的解决方法。 我喜欢这个选项,因为我在网络客户端上以相同的方式解析一堆模型,并且我希望解决方案是其中一个对象的本地解决方案。也就是说,我仍然希望其他的都被丢弃。

我在我的githubhttps://github.com/phynet/Lossy-array-decode-swift4解释得更好

import Foundation

    let json = """
    [
        
            "name": "Banana",
            "points": 200,
            "description": "A banana grown in Ecuador."
        ,
        
            "name": "Orange"
        
    ]
    """.data(using: .utf8)!

    private struct DummyCodable: Codable 

    struct Groceries: Codable 
    
        var groceries: [GroceryProduct]

        init(from decoder: Decoder) throws 
            var groceries = [GroceryProduct]()
            var container = try decoder.unkeyedContainer()
            while !container.isAtEnd 
                if let route = try? container.decode(GroceryProduct.self) 
                    groceries.append(route)
                 else 
                    _ = try? container.decode(DummyCodable.self) // <-- TRICK
                
            
            self.groceries = groceries
        
    

    struct GroceryProduct: Codable 
        var name: String
        var points: Int
        var description: String?
    

    let products = try JSONDecoder().decode(Groceries.self, from: json)

    print(products)

【讨论】:

一种变体,而不是if/else,我在while 循环内使用do/catch,以便记录错误 这个答案提到了 Swift 错误跟踪器并且有最简单的附加结构(没有泛型!)所以我认为它应该是被接受的。 这应该是公认的答案。任何破坏数据模型的答案都是不可接受的权衡。【参考方案5】:

我已经将@sophy-swicz 解决方案,经过一些修改,放入一个易于使用的扩展中

fileprivate struct DummyCodable: Codable 

extension UnkeyedDecodingContainer 

    public mutating func decodeArray<T>(_ type: T.Type) throws -> [T] where T : Decodable 

        var array = [T]()
        while !self.isAtEnd 
            do 
                let item = try self.decode(T.self)
                array.append(item)
             catch let error 
                print("error: \(error)")

                // hack to increment currentIndex
                _ = try self.decode(DummyCodable.self)
            
        
        return array
    

extension KeyedDecodingContainerProtocol 
    public func decodeArray<T>(_ type: T.Type, forKey key: Self.Key) throws -> [T] where T : Decodable 
        var unkeyedContainer = try self.nestedUnkeyedContainer(forKey: key)
        return try unkeyedContainer.decodeArray(type)
    

就这样称呼吧

init(from decoder: Decoder) throws 

    let container = try decoder.container(keyedBy: CodingKeys.self)

    self.items = try container.decodeArray(ItemType.self, forKey: . items)

对于上面的例子:

let json = """
[
    
        "name": "Banana",
        "points": 200,
        "description": "A banana grown in Ecuador."
    ,
    
        "name": "Orange"
    
]
""".data(using: .utf8)!

struct Groceries: Codable 

    var groceries: [GroceryProduct]

    init(from decoder: Decoder) throws 
        var container = try decoder.unkeyedContainer()
        groceries = try container.decodeArray(GroceryProduct.self)
    


struct GroceryProduct: Codable 
    var name: String
    var points: Int
    var description: String?


let products = try JSONDecoder().decode(Groceries.self, from: json)
print(products)

【讨论】:

我已将此解决方案封装在扩展程序 github.com/IdleHandsApps/SafeDecoder【参考方案6】:

我会创建一个新类型Throwable,它可以包装任何符合Decodable的类型:

enum Throwable<T: Decodable>: Decodable 
    case success(T)
    case failure(Error)

    init(from decoder: Decoder) throws 
        do 
            let decoded = try T(from: decoder)
            self = .success(decoded)
         catch let error 
            self = .failure(error)
        
    

用于解码GroceryProduct(或任何其他Collection)的数组:

let decoder = JSONDecoder()
let throwables = try decoder.decode([Throwable<GroceryProduct>].self, from: json)
let products = throwables.compactMap  $0.value 

其中value 是在Throwable 的扩展中引入的计算属性:

extension Throwable 
    var value: T? 
        switch self 
        case .failure(_):
            return nil
        case .success(let value):
            return value
        
    

我会选择使用 enum 包装器类型(而不是 Struct),因为跟踪引发的错误及其索引可能很有用。

斯威夫特 5

对于 Swift 5 考虑使用 Result enum 例如

struct Throwable<T: Decodable>: Decodable 
    let result: Result<T, Error>

    init(from decoder: Decoder) throws 
        result = Result(catching:  try T(from: decoder) )
    

要解开解码后的值,请在 result 属性上使用 get() 方法:

let products = throwables.compactMap  try? $0.result.get() 

【讨论】:

我喜欢这个答案,因为我不必担心编写任何自定义 init 这是我一直在寻找的解决方案。它是如此的干净和直接。谢谢你! 好方法。它真的帮助我把工作做好。谢谢。【参考方案7】:

我想出了这个KeyedDecodingContainer.safelyDecodeArray,它提供了一个简单的界面:

extension KeyedDecodingContainer 

/// The sole purpose of this `EmptyDecodable` is allowing decoder to skip an element that cannot be decoded.
private struct EmptyDecodable: Decodable 

/// Return successfully decoded elements even if some of the element fails to decode.
func safelyDecodeArray<T: Decodable>(of type: T.Type, forKey key: KeyedDecodingContainer.Key) -> [T] 
    guard var container = try? nestedUnkeyedContainer(forKey: key) else 
        return []
    
    var elements = [T]()
    elements.reserveCapacity(container.count ?? 0)
    while !container.isAtEnd 
        /*
         Note:
         When decoding an element fails, the decoder does not move on the next element upon failure, so that we can retry the same element again
         by other means. However, this behavior potentially keeps `while !container.isAtEnd` looping forever, and Apple does not offer a `.skipFailable`
         decoder option yet. As a result, `catch` needs to manually skip the failed element by decoding it into an `EmptyDecodable` that always succeed.
         See the Swift ticket https://bugs.swift.org/browse/SR-5953.
         */
        do 
            elements.append(try container.decode(T.self))
         catch 
            if let decodingError = error as? DecodingError 
                Logger.error("\(#function): skipping one element: \(decodingError)")
             else 
                Logger.error("\(#function): skipping one element: \(error)")
            
            _ = try? container.decode(EmptyDecodable.self) // skip the current element by decoding it into an empty `Decodable`
        
    
    return elements


潜在的无限循环while !container.isAtEnd 是一个问题,可以使用EmptyDecodable 解决。

【讨论】:

【参考方案8】:

一个更简单的尝试: 为什么不将点声明为可选或使数组包含可选元素

let products = [GroceryProduct?]

【讨论】:

【参考方案9】:

@Hamish 的回答很棒。但是,您可以将FailableCodableArray 减少为:

struct FailableCodableArray<Element : Codable> : Codable 

    var elements: [Element]

    init(from decoder: Decoder) throws 
        let container = try decoder.singleValueContainer()
        let elements = try container.decode([FailableDecodable<Element>].self)
        self.elements = elements.compactMap  $0.wrapped 
    

    func encode(to encoder: Encoder) throws 
        var container = encoder.singleValueContainer()
        try container.encode(elements)
    

【讨论】:

我最喜欢的答案。不过,使用示例可能会有所帮助。【参考方案10】:

我最近遇到了类似的问题,但略有不同。

struct Person: Codable 
    var name: String
    var age: Int
    var description: String?
    var friendnamesArray:[String]?

在这种情况下,如果friendnamesArray 中的元素之一为nil,则解码时整个对象为nil。

处理这种极端情况的正确方法是将字符串数组[String] 声明为可选字符串数组[String?],如下所示,

struct Person: Codable 
    var name: String
    var age: Int
    var description: String?
    var friendnamesArray:[String?]?

【讨论】:

【参考方案11】:

我改进了 @Hamish 的案例,你希望所有数组都有这种行为:

private struct OptionalContainer<Base: Codable>: Codable 
    let base: Base?
    init(from decoder: Decoder) throws 
        let container = try decoder.singleValueContainer()
        base = try? container.decode(Base.self)
    


private struct OptionalArray<Base: Codable>: Codable 
    let result: [Base]
    init(from decoder: Decoder) throws 
        let container = try decoder.singleValueContainer()
        let tmp = try container.decode([OptionalContainer<Base>].self)
        result = tmp.compactMap  $0.base 
    


extension Array where Element: Codable 
    init(from decoder: Decoder) throws 
        let optionalArray = try OptionalArray<Element>(from: decoder)
        self = optionalArray.result
    

【讨论】:

【参考方案12】:

Swift 5.1 使用属性包装器实现的解决方案:

@propertyWrapper
struct IgnoreFailure<Value: Decodable>: Decodable 
    var wrappedValue: [Value] = []

    private struct _None: Decodable 

    init(from decoder: Decoder) throws 
        var container = try decoder.unkeyedContainer()
        while !container.isAtEnd 
            if let decoded = try? container.decode(Value.self) 
                wrappedValue.append(decoded)
            
            else 
                // item is silently ignored.
                try? container.decode(_None.self)
            
        
    

然后是用法:

let json = """

    "products": [
        
            "name": "Banana",
            "points": 200,
            "description": "A banana grown in Ecuador."
        ,
        
            "name": "Orange"
        
    ]

""".data(using: .utf8)!

struct GroceryProduct: Decodable 
    var name: String
    var points: Int
    var description: String?


struct ProductResponse: Decodable 
    @IgnoreFailure
    var products: [GroceryProduct]



let response = try! JSONDecoder().decode(ProductResponse.self, from: json)
print(response.products) // Only contains banana.

注意:属性包装的东西只有在响应可以包装在结构中时才有效(即:不是***数组)。 在这种情况下,您仍然可以手动包装它(使用 typealias 以提高可读性):

typealias ArrayIgnoringFailure<Value: Decodable> = IgnoreFailure<Value>

let response = try! JSONDecoder().decode(ArrayIgnoringFailure<GroceryProduct>.self, from: json)
print(response.wrappedValue) // Only contains banana.

【讨论】:

【参考方案13】:

相反,您也可以这样做:

struct GroceryProduct: Decodable 
    var name: String
    var points: Int
    var description: String?
'

然后在得到它的时候进入:

'let groceryList = try JSONDecoder().decode(Array<GroceryProduct>.self, from: responseData)'

【讨论】:

【参考方案14】:

特点:

使用简单。可解码实例中的一行:let array: CompactDecodableArray&lt;Int&gt; 使用标准映射机制解码:JSONDecoder().decode(Model.self, from: data) 跳过不正确的元素(返回仅包含成功映射元素的数组)

详情

Xcode 12.1 (12A7403) 斯威夫特 5.3

解决方案

class CompactDecodableArray<Element>: Decodable where Element: Decodable 
    private(set) var elements = [Element]()
    required init(from decoder: Decoder) throws 
        guard var unkeyedContainer = try? decoder.unkeyedContainer() else  return 
        while !unkeyedContainer.isAtEnd 
            if let value = try? unkeyedContainer.decode(Element.self) 
                elements.append(value)
             else 
                unkeyedContainer.skip()
            
        
    


// https://forums.swift.org/t/pitch-unkeyeddecodingcontainer-movenext-to-skip-items-in-deserialization/22151/17

struct Empty: Decodable  

extension UnkeyedDecodingContainer 
    mutating func skip()  _ = try? decode(Empty.self) 

用法

struct Model2: Decodable 
    let num: Int
    let str: String


struct Model: Decodable 
    let num: Int
    let str: String
    let array1: CompactDecodableArray<Int>
    let array2: CompactDecodableArray<Int>?
    let array4: CompactDecodableArray<Model2>


let dictionary: [String : Any] = ["num": 1, "str": "blablabla",
                                  "array1": [1,2,3],
                                  "array3": [1,nil,3],
                                  "array4": [["num": 1, "str": "a"], ["num": 2]]
]

let data = try! JSONSerialization.data(withJSONObject: dictionary)
let object = try JSONDecoder().decode(Model.self, from: data)
print("1. \(object.array1.elements)")
print("2. \(object.array2?.elements)")
print("3. \(object.array4.elements)")

控制台

1. [1, 2, 3]
2. nil
3. [__lldb_expr_25.Model2(num: 1, str: "a")]

【讨论】:

【参考方案15】:

斯威夫特 5

受我之前在 Result 枚举扩展中解码的答案的启发。

你怎么看?


extension Result: Decodable where Success: Decodable, Failure == DecodingError 

    public init(from decoder: Decoder) throws 

        let container: SingleValueDecodingContainer = try decoder.singleValueContainer()

        do 

            self = .success(try container.decode(Success.self))

         catch 

            if let decodingError = error as? DecodingError 
                self = .failure(decodingError)
             else 
                self = .failure(DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: error.localizedDescription)))
            
        
    
    



用法


let listResult = try? JSONDecoder().decode([Result<SomeObject, DecodingError>].self, from: ##YOUR DATA##)

let list: [SomeObject] = listResult.compactMap try? $0.get()


【讨论】:

这是一个不错的解决方案,它使用内置的结果类型。减少编写代码。非常好。【参考方案16】:

您将描述设为可选,如果有可能为 nil,您还应该将 points 字段设为可选,例如:

struct GroceryProduct: Codable 
    var name: String
    var points: Int?
    var description: String?

只要确保你安全地打开它,你认为它适合它的使用。我猜在实际用例中 nil points == 0 所以一个例子可能是:

let products = try JSONDecoder().decode([GroceryProduct].self, from: json)
for product in products 
    let name = product.name
    let points = product.points ?? 0
    let description = product.description ?? ""
    ProductView(name, points, description)

或在线:

let products = try JSONDecoder().decode([GroceryProduct].self, from: json)
for product in products 
    ProductView(product.name, product.points ?? 0, product.description ?? "")

【讨论】:

以上是关于如果单元素解码失败,Swift JSONDecode 解码数组将失败的主要内容,如果未能解决你的问题,请参考以下文章

获取 API 时解码失败 |迅速

通过使用 Alamofire 和解码获取 JSON - Swift 4

使用 JSONDecoder 解码 [[String]]?

Swift:正确解码不精确的十进制

Swift 5 默认可解码实现,只有一个例外

如何从 Swift 4 中的解码器容器中获取未解码的属性?