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

时间:2017-09-21 13:16:03

标签: arrays json swift swift4 codable

在使用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"密钥,而points结构中GroceryProduct不是可选的,因此可以预料到这一点。

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

16 个答案:

答案 0 :(得分:79)

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

答案 1 :(得分:16)

有两种选择:

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

答案 2 :(得分:16)

问题是当迭代容器时,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)

答案 3 :(得分:10)

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

答案 4 :(得分:6)

我已将@ 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)

答案 5 :(得分:3)

相反,您也可以这样:

public ApplicationDbContext() : base("DefaultConnection")
{
}

然后在获取它的时候进来:

## inventory var ##
 inv=["torch"] 


## inventory function ##
 def ichk():
       print(f" You are carrying{inv}")
       return



##First room, links to a3##

    def a2():
    
    move=input("placeholder text.. You notice an exit to your 'east': ")

    if move==("e"):
            print("You exit to the east")
            a3()
    elif move==("i"):
            ichk()

    elif move==("q"):
            print("You find nothing of value")
            a2()
    else:
            print("You can't move in that direction")
            a2()

答案 6 :(得分:2)

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

答案 7 :(得分: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
    }
}

答案 8 :(得分:2)

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.

答案 9 :(得分: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)
    }
}

答案 10 :(得分: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来解决。

答案 11 :(得分:0)

我最近有一个类似的问题,但略有不同。

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?]?
}

答案 12 :(得分:0)

详细信息

  • Xcode 12.1(12A7403)
  • Swift 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")]

答案 13 :(得分:0)

斯威夫特 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()}


答案 14 :(得分:-1)

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

fruits="$3"
checkFruits= sed -i 's/,/ /g' <<< "$fruits"
echo $checkFruits

答案 15 :(得分:-3)

我遇到了同样的问题,没有一个满意的答复。

我有以下结构:

public struct OfferResponse {
public private(set) var offers: [Offer]

public init(data: Data) throws {
    let json = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: [Any]]
    guard let offersDataArray = json?["Offers"] else {
        throw NSError(domain: "unexpected JSON structure for \(type(of: self))", code: 36, userInfo: nil)
    }
    guard let firstOfferData = offersDataArray.first else {
        throw NSError(domain: "emptyArray in JSON structure for \(type(of: self))", code: 36, userInfo: nil)
    }
    let decoder = JSONDecoder()
    offers = try decoder.decode([Offer].self, from: JSONSerialization.data(withJSONObject: firstOfferData, options: .prettyPrinted))
}

在某一时刻,后端返回了元素的错误内容。 我是这样解决的:

    offers = []
    for offerData in offersDataArray {
        if let offer = try? decoder.decode(Offer.self, from: JSONSerialization.data(withJSONObject: offerData, options: .prettyPrinted)) {
            offers.append(offer)
        }