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

169

在使用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)

由于JSON的第二个对象中没有"points"键,而在GroceryProduct结构中,points是必需的,因此导致生成的产品为空,这是可以预料的。

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


我们不能跳过无效的对象,但是如果它为nil,你可以分配默认值。 - Vini App
3
为什么不能将 points 声明为可选项? - NRitH
3
因为有时候缺少一个字段是没有意义的,而将其设置为可选项会破坏你的模型。 - Martin
16个回答

159

一种选择是使用一个包装类型尝试解码给定的值;如果不成功,存储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
如果基础对象不是一个数组,但它包含一个数组怎么办?比如 { "products": [{"name": "banana"...},...] } - ludvigeriksson
2
@ludvigeriksson,您只需要在该结构内执行解码操作,例如:https://gist.github.com/hamishknight/c6d270f7298e4db9e787aecb5b98bcae - Hamish
2
Swift的Codable以前很容易,但现在有点复杂了..难道不能再简单一点吗? - Jonny
@Hamish,我没有看到这行代码的任何错误处理。如果在此处抛出错误会发生什么情况 var container = try decoder.unkeyedContainer() - bibscy
@bibscy 这段代码位于 init(from:) throws 函数体内,因此 Swift 会自动将错误传播回调用者(在本例中是解码器,它会将错误传播回 JSONDecoder.decode(_:from:) 调用)。 - Hamish
显示剩余3条评论

58
我会创建一个新的类型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

对于 Swift 5,考虑使用 Result 枚举类型,例如:

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

1
我喜欢这个答案,因为我不必担心编写任何自定义的“init”。 - Mihai Fratu
1
这就是我一直在寻找的解决方案。它非常简洁明了。谢谢你! - naturaln0va
好的方式。它真正帮助我完成了我的工作。谢谢。 - boraseoksoon

29

问题在于,当迭代容器时,容器的currentIndex不会自增,因此您可能会尝试使用不同类型再次解码。

由于currentIndex是只读的,解决方案是自己递增并成功解码一个虚拟元素。我采用了@Hamish的解决方案,并编写了一个带有自定义init的包装器。

这个问题是当前Swift的一个bug: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
一个变化是,我在 while 循环内部使用 do/catch 而不是 if/else,这样我就可以记录错误日志。 - Fraser
2
这个答案提到了Swift bug跟踪器,并且有最简单的附加结构(没有泛型!),所以我认为它应该被接受。 - Alper
2
这应该是被接受的答案。任何破坏您数据模型的答案都是不可接受的折衷方案,以我个人看来。 - Joe Susnick

24

有两个选项:

  1. 声明结构体的所有成员为可选,其中可能会缺少某些键。

    struct GroceryProduct: Codable {
        var name: String
        var points : Int?
        var description: String?
    }
    
    编写一个自定义初始化器,在 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
第二种选择中,与decode一起使用try?相比,最好使用decodeIfPresenttry。只有在没有键时才需要设置默认值,而不是在任何解码失败的情况下,例如当键存在但类型错误时。 - user28434'mstep
嘿@vadian,你知道还有哪些SO问题涉及自定义初始化器以分配默认值,以防类型不匹配吗?我有一个键是Int类型,但有时在JSON中会是String类型,所以我尝试按照你上面说的做法进行deviceName = try values.decodeIfPresent(Int.self, forKey: .deviceName) ?? 00000,所以如果失败了,它就会把0000放进去,但它仍然失败了。 - Martheli
在这种情况下,decodeIfPresent 是错误的 API,因为键确实存在。使用另一个 do-catch 块。如果出现错误,请解码 String,然后解码 Int - vadian

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

然后是使用方法:

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 提高可读性):

注意:只有响应可以被包装在结构体中时(即不是顶层数组),属性包装器才能生效。在这种情况下,您仍然可以手动进行包装(使用 typealias 可以提高可读性):

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

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


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

我已经将这个解决方案封装在一个扩展中 https://github.com/IdleHandsApps/SafeDecoder - Fraser

6

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



这是一个不错的解决方案,使用内置的结果类型。写的代码更少,非常棒。 - JoeBayLD

5

相反,你也可以这样做:

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

然后在获取它的同时:

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

4

很遗憾,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)")
}

4

如果有可能出现空值的情况,比如这种情况,您已经将描述字段设为可选项,请也将点数字段设为可选项:

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

请确保您以适合使用的方式安全解包它。我猜在实际用例中,nil points等于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 ?? "")
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接