如果单元素解码失败,则Swift JSONDecode解码数组也会失败


116

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

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

和一个可编码的结构:

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"键,而pointsGroceryProductstruct中不是可选的。

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


我们不能跳过无效的对象,但是如果它为nil,则可以分配默认值。
Vini App

1
为什么不能points仅仅宣布为可选?
NRitH

Answers:


115

一种选择是使用包装器类型,尝试对给定值进行解码。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)
    }
}

然后,我们可以解码这些数组,并GroceryProductBase占位符中填写以下内容:

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<GroceryProduct>],这不应该成为问题;但是,如果您希望避免这种情况,可以始终创建另一个包装器类型,该类型将解码和取消包装来自非键控容器的每个元素:

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.")
//    )
// ]

1
如果基础对象不是数组而是包含一个数组怎么办?像{“产品”:[{“名称”:“香蕉” ...},...]一样
ludvigeriksson

2
@ludvigeriksson您只想在该结构中执行解码,例如:gist.github.com/hamishknight/c6d270f7298e4db9e787aecb5b98bcae
Hamish

1
Swift的Codable很简单,直到现在..难道这一点都不简单吗?
强尼

@Hamish我没有看到此行的任何错误处理。如果在这里引发错误,该怎么办var container = try decoder.unkeyedContainer()
bibscy

@bibscy它位于的主体内init(from:) throws,因此Swift会将错误自动传播回调用者(在本例中为解码器,该错误会将其传播回JSONDecoder.decode(_:from:)调用)。
Hamish

33

我将创建一个新类型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,请考虑使用egResult enum

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

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

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

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

我喜欢这个答案,因为我不必担心会写任何习俗init
Mihai Fratu

这是我一直在寻找的解决方案。它是如此干净直接。这次真是万分感谢!
naturaln0va

24

问题在于,在容器上进行迭代时,container.currentIndex不会增加,因此您可以尝试使用其他类型再次进行解码。

因为currentIndex是只读的,所以一种解决方案是自己成功解码虚拟对象以对其进行递增。我使用@Hamish解决方案,并使用自定义init编写了包装器。

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

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

我在我的github https://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)

1
一种变化形式,而不是if/else我使用的do/catchwhile循环内部,因此我可以记录错误
Fraser

2
这个答案提到了Swift错误跟踪器,并且具有最简单的附加结构(没有泛型!),因此我认为它应该是公认的。
Alper

2
这应该是公认的答案。任何破坏数据模型的答案都是不可接受的折衷方案。
Joe Susnick

21

有两种选择:

  1. 将结构的所有成员声明为可选,其键可能会丢失

    struct GroceryProduct: Codable {
        var name: String
        var points : Int?
        var description: String?
    }
  2. 在这种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) ?? ""
        }
    }

5
与其try?一起decode使用try,不如decodeIfPresent在第二个选项中使用。我们仅在没有密钥的情况下才需要设置默认值,而在任何解码失败的情况下(例如当密钥存在但类型错误时)则不需要设置默认值。
user28434

嘿@vadian您是否知道其他涉及自定义初始化程序的问题,以便在类型不匹配的情况下分配默认值?我有一个Int密钥,但有时会是JSON中的字符串,所以我尝试按照上面的说明进行操作,deviceName = try values.decodeIfPresent(Int.self, forKey: .deviceName) ?? 00000因此,如果失败,它将只放入0000,但仍然失败。
Martheli

在这种情况下decodeIfPresent是错误的,API因为密钥确实存在。使用另一个do - catch块。解码String,如果发生错误,则解码Int
vadian

13

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 ArrayIgnoringFailure<Value: Decodable> = IgnoreFailure<Value>

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

7

Ive将@ 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中
Fraser,

3

不幸的是,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 Person: Codable {
    var name: String
    var age: Int
    var description: String?
    var friendnamesArray:[String]?
}

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

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

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

2

对于这种情况,我对@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
    }
}

1

@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)
    }
}

1

相反,您也可以这样:

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

然后在获取它的同时:

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

0

我想出了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


0

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

let products = [GroceryProduct?]
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.