如果单个元素解码失败,则迅速 JSONDecode 解码阵列失败

在使用 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"键,而 pointsGroceryProduct结构中不是可选的。

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

94506 次浏览

不幸的是,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)")
}

有两种选择:

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

    struct GroceryProduct: Codable {
    var name: String
    var points : Int?
    var description: String?
    }
    
  2. Write a custom initializer to assign default values in the nil case.

    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) ?? ""
    }
    }
    

一种选择是使用试图解码给定值的包装类型; 如果不成功,则存储 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<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.")
//    )
// ]

问题是,当在容器上迭代时,conter.currentIndex 没有递增,因此您可以尝试用不同的类型再次进行解码。

因为 currentIndex 是只读的,所以解决方案是自己增量它,成功地解码一个虚拟机。我采用了@Hamish 解决方案,并编写了一个带有自定义 init 的包装器。

这个问题是目前 Swift 的 bug: https://bugs.swift.org/browse/SR-5953

这里发布的解决方案是在其中一条评论中提供的解决方案。 我喜欢这个选项,因为我在网络客户机上以相同的方式解析一组模型,我希望解决方案是其中一个对象的本地解决方案。也就是说,我还是希望其他人被抛弃。

我解释得更清楚了

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)

我把@sopy-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)

我将创建一个新类型 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) ,因为跟踪抛出的错误及其索引可能是有用的。

Swift 5

为迅捷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() }

我想出了这个 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来解决。

一个更简单的尝试: 为什么不把点声明为可选的,或者让数组包含可选的元素呢

let products = [GroceryProduct?]

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

我最近也遇到过类似的问题,只是稍有不同。

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

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

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

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

我在@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
}
}

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.


相反,你也可以这样做:

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

然后在拿的时候进去:

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

特点:

  • 简单使用。可解码实例中的一行: let array: CompactDecodableArray<Int>
  • 使用标准映射机制解码: 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")]

Swift 5

灵感来自于前面的答案,我在 Resultenum 扩展中解码。

你觉得怎么样?


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




你让描述成为可选的,你也应该让点字段成为可选的,如果有可能它可能是空的,比如:

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

只要确保你安全-打开它,只要你认为适合它的使用。我猜测在实际的用例中,零点 = = 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 ?? "")
}