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

Khr*_*riy 86 arrays json swift swift4 codable

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

[
    {
        "name": "Banana",
        "points": 200,
        "description": "A banana grown in Ecuador."
    },
    {
        "name": "Orange"
    }
]
Run Code Online (Sandbox Code Playgroud)

还有一个Codable结构:

struct GroceryProduct: Codable {
    var name: String
    var points: Int
    var description: String?
}
Run Code Online (Sandbox Code Playgroud)

解码这个json时

let decoder = JSONDecoder()
let products = try decoder.decode([GroceryProduct].self, from: json)
Run Code Online (Sandbox Code Playgroud)

结果products是空的.这是可以预料到的,因为JSON中的第二个对象没有"points"键,而struct中points不是可选的GroceryProduct.

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

Ham*_*ish 93

一种选择是使用尝试解码给定值的包装器类型; 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)
    }
}
Run Code Online (Sandbox Code Playgroud)

然后我们可以解码这些数组,并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.")
//    )
// ]
Run Code Online (Sandbox Code Playgroud)

然后我们.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)
    }
}
Run Code Online (Sandbox Code Playgroud)

然后你会解码为:

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.")
//    )
// ]
Run Code Online (Sandbox Code Playgroud)

  • @ludvigeriksson您只想在该结构中执行解码,例如:https://gist.github.com/hamishknight/c6d270f7298e4db9e787aecb5b98bcae (2认同)
  • Swift 的 Codable 很简单,直到现在......这不能变得更简单一点吗? (2认同)

Sop*_*icz 23

问题是,当迭代容器时,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)
Run Code Online (Sandbox Code Playgroud)

  • 这个答案提到了 Swift 错误跟踪器,并且有最简单的附加结构(没有泛型!)所以我认为它应该是公认的。 (2认同)
  • 这应该是公认的答案。任何破坏您的数据模型的答案都是不可接受的权衡imo。 (2认同)

cfe*_*gie 23

我会创建一个新类型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)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

let decoder = JSONDecoder()
let throwables = try decoder.decode([Throwable<GroceryProduct>].self, from: json)
let products = throwables.compactMap { $0.value }
Run Code Online (Sandbox Code Playgroud)

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

extension Throwable {
    var value: T? {
        switch self {
        case .failure(_):
            return nil
        case .success(let value):
            return value
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我会选择使用enum包装器类型(通过a Struct),因为跟踪抛出的错误及其索引可能很有用.


vad*_*ian 17

有两种选择:

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

    struct GroceryProduct: Codable {
        var name: String
        var points : Int?
        var description: String?
    }
    
    Run Code Online (Sandbox Code Playgroud)
  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) ?? ""
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)

  • 而不是`try?`with`decode`,最好在第二个选项中使用`try`和`decodeIfPresent`.我们只需要在没有密钥的情况下设置默认值,而不是在任何解码失败的情况下,例如当密钥存在时,但是类型错误. (4认同)

rra*_*ael 17

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)
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

然后是用法:

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.

Run Code Online (Sandbox Code Playgroud)

注意:只有当响应可以包装在结构中(即:不是顶级数组)时,属性包装器才有效。在这种情况下,您仍然可以手动包装它(使用类型别名以获得更好的可读性):

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

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

Run Code Online (Sandbox Code Playgroud)


Fra*_*ser 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)
    }
}
Run Code Online (Sandbox Code Playgroud)

就这样称呼它

init(from decoder: Decoder) throws {

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

    self.items = try container.decodeArray(ItemType.self, forKey: . items)
}
Run Code Online (Sandbox Code Playgroud)

对于上面的例子:

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)
Run Code Online (Sandbox Code Playgroud)


Ale*_*pov 6

雨燕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)))
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

用法

let listResult = try? JSONDecoder().decode([Result<SomeObject, DecodingError>].self, from: ##YOUR DATA##)
let list: [SomeObject] = listResult.compactMap {try? $0.get()}
Run Code Online (Sandbox Code Playgroud)


小智 5

相反,您也可以这样做:

struct GroceryProduct: Decodable {
    var name: String
    var points: Int
    var description: String?
}'
Run Code Online (Sandbox Code Playgroud)

然后在获取它的同时:

'let groceryList = try JSONDecoder().decode(Array<GroceryProduct>.self, from: responseData)'
Run Code Online (Sandbox Code Playgroud)