如果单个元素解码失败,Swift JSONDecode 解码数组就会失败
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/46344963/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Swift JSONDecode decoding arrays fails if single element decoding fails
提问by Khriapin Dmitriy
While using Swift4 and Codable protocols I got the following problem - it looks like there is no way to allow JSONDecoderto skip elements in an array.
For example, I have the following JSON:
在使用 Swift4 和 Codable 协议时,我遇到了以下问题 - 似乎无法允许JSONDecoder跳过数组中的元素。例如,我有以下 JSON:
[
{
"name": "Banana",
"points": 200,
"description": "A banana grown in Ecuador."
},
{
"name": "Orange"
}
]
And a Codablestruct:
和一个Codable结构:
struct GroceryProduct: Codable {
var name: String
var points: Int
var description: String?
}
When decoding this json
解码这个json时
let decoder = JSONDecoder()
let products = try decoder.decode([GroceryProduct].self, from: json)
Resulting productsis empty. Which is to be expected, due to the fact that the second object in JSON has no "points"key, while pointsis not optional in GroceryProductstruct.
结果products是空的。这是可以预料的,因为 JSON 中的第二个对象没有"points"键,而points在GroceryProduct结构中不是可选的。
Question is how can I allow JSONDecoderto "skip" invalid object?
问题是如何允许JSONDecoder“跳过”无效对象?
回答by Hamish
One option is to use a wrapper type that attempts to decode a given value; storing nilif unsuccessful:
一种选择是使用尝试解码给定值的包装器类型;存储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)
}
}
We can then decode an array of these, with your GroceryProductfilling in the Baseplaceholder:
然后我们可以解码这些数组,并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 { 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)
}
}
.base } // .flatMap in Swift 4.0
print(products)
// [
// GroceryProduct(
// name: "Banana", points: 200,
// description: Optional("A banana grown in Ecuador.")
// )
// ]
We're then using .compactMap { $0.base }to filter out nilelements (those that threw an error on decoding).
然后我们使用.compactMap { $0.base }过滤掉nil元素(那些在解码时抛出错误的元素)。
This will create an intermediate array of [FailableDecodable<GroceryProduct>], which shouldn't be an issue; however if you wish to avoid it, you could always create another wrapper type that decodes and unwraps each element from an unkeyed container:
这将创建一个中间数组[FailableDecodable<GroceryProduct>],这应该不是问题;但是,如果您希望避免它,您始终可以创建另一种包装器类型,用于解码和解开未加密容器中的每个元素:
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.")
// )
// ]
You would then decode as:
然后您将解码为:
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)
}
}
}
回答by cfergie
I would create a new type Throwable, which can wrap any type conforming to Decodable:
我会创建一个新类型Throwable,它可以包装任何符合Decodable以下条件的类型:
let decoder = JSONDecoder()
let throwables = try decoder.decode([Throwable<GroceryProduct>].self, from: json)
let products = throwables.compactMap { extension Throwable {
var value: T? {
switch self {
case .failure(_):
return nil
case .success(let value):
return value
}
}
}
.value }
For decoding an array of GroceryProduct(or any other Collection):
用于解码GroceryProduct(或任何其他Collection)数组:
struct Throwable<T: Decodable>: Decodable {
let result: Result<T, Error>
init(from decoder: Decoder) throws {
result = Result(catching: { try T(from: decoder) })
}
}
where valueis a computed property introduced in an extension on Throwable:
在哪里value是在扩展中引入的计算属性Throwable:
let products = throwables.compactMap { try? 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)
.result.get() }
I would opt for using a enumwrapper type (over a Struct) because it may be useful to keep track of the errors that are thrown as well as their indices.
我会选择使用enum包装器类型(在 a 上Struct),因为跟踪抛出的错误及其索引可能很有用。
Swift 5
斯威夫特 5
For Swift 5 Consider using the Resultenume.g.
对于 Swift 5 考虑使用egResultenum
struct GroceryProduct: Codable {
var name: String
var points : Int?
var description: String?
}
To unwrap the decoded value use the get()method on the resultproperty:
要解开解码的值,请使用属性get()上的方法result:
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) ?? ""
}
}
回答by Sophy Swicz
The problem is that when iterating over a container, the container.currentIndex isn't incremented so you can try to decode again with a different type.
问题在于,在迭代容器时, container.currentIndex 不会增加,因此您可以尝试使用不同的类型再次解码。
Because the currentIndex is read only, a solution is to increment it yourself successfully decoding a dummy. I took @Hamish solution, and wrote a wrapper with a custom init.
因为 currentIndex 是只读的,一个解决方案是自己增加它,成功解码一个哑元。我采用了@Hamish 解决方案,并用自定义初始化编写了一个包装器。
This problem is a current Swift bug: https://bugs.swift.org/browse/SR-5953
此问题是当前的 Swift 错误:https: //bugs.swift.org/browse/SR-5953
The solution posted here is a workaround in one of the comments. I like this option because I'm parsing a bunch of models the same way on a network client, and I wanted the solution to be local to one of the objects. That is, I still want the others to be discarded.
此处发布的解决方案是其中一条评论中的解决方法。我喜欢这个选项,因为我在网络客户端上以相同的方式解析一堆模型,并且我希望解决方案是其中一个对象的本地解决方案。也就是说,我仍然希望其他人被丢弃。
I explain better in my github https://github.com/phynet/Lossy-array-decode-swift4
我在我的 github https://github.com/phynet/Lossy-array-decode-swift4 中解释得更好
struct GroceryProduct: Codable {
var name: String
var points : Int?
var description: String?
}
回答by vadian
There are two options:
有两种选择:
Declare all members of the struct as optional whose keys can be missing
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) ?? "" } }Write a custom initializer to assign default values in the
nilcase.@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.编写自定义初始值设定项以在
nil案例中分配默认值。typealias ArrayIgnoringFailure<Value: Decodable> = IgnoreFailure<Value> let response = try! JSONDecoder().decode(ArrayIgnoringFailure<GroceryProduct>.self, from: json) print(response.wrappedValue) // Only contains banana.
回答by rraphael
A solution made possible by Swift 5.1, using the property wrapper:
Swift 5.1 使用属性包装器实现的解决方案:
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)
}
}
And then the usage:
然后是用法:
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.items = try container.decodeArray(ItemType.self, forKey: . items)
}
Note: The property wrapper things will only works if the response can be wrapped in a struct (i.e: not a top level array). In that case, you can still wrap it manually (with a typealias for better readability):
注意:只有当响应可以包装在结构中(即:不是顶级数组)时,属性包装器才有效。在这种情况下,您仍然可以手动包装它(使用类型别名以获得更好的可读性):
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)
回答by Fraser
Ive put @sophy-swicz solution, with some modifications, into an easy to use extension
我已经将@sophy-swicz 解决方案(经过一些修改)放入了一个易于使用的扩展程序中
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 { struct Person: Codable {
var name: String
var age: Int
var description: String?
var friendnamesArray:[String]?
}
.points != nil }
print("clearedResult: \(clearedResult)")
}
Just call it like this
就这样称呼
struct Person: Codable {
var name: String
var age: Int
var description: String?
var friendnamesArray:[String?]?
}
For the example above:
对于上面的例子:
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 { 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 { ##代码##.wrapped }
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(elements)
}
}
.base }
}
}
extension Array where Element: Codable {
init(from decoder: Decoder) throws {
let optionalArray = try OptionalArray<Element>(from: decoder)
self = optionalArray.result
}
}
回答by dimpiax
Unfortunately Swift 4 API doesn't have failable initializer for init(from: Decoder).
不幸的是,Swift 4 API 没有用于init(from: Decoder).
Only one solution that I see is implementing custom decoding, giving default value for optional fields and possible filter with needed data:
我看到的只有一种解决方案是实现自定义解码,为可选字段提供默认值并使用所需数据进行可能的过滤:
##代码##回答by cnu
I had a similar issue recently, but slightly different.
我最近遇到了类似的问题,但略有不同。
##代码##In this case, if one of the element in friendnamesArrayis nil, the whole object is nil while decoding.
在这种情况下,如果 in 中的元素之一friendnamesArray为零,则整个对象在解码时为零。
And the right way to handle this edge case is to declare the string array[String]as array of optional strings[String?]as below,
处理这种边缘情况的正确方法是将字符串数组声明[String]为可选字符串数组,[String?]如下所示,
回答by S?ren Schmaljohann
I improved on @Hamish's for the case, that you want this behaviour for all arrays:
我改进了@Hamish 的案例,你希望所有数组都有这种行为:
##代码##回答by Robert Crabtree
@Hamish's answer is great. However, you can reduce FailableCodableArrayto:
@Hamish 的回答很棒。但是,您可以减少FailableCodableArray到:

