Mảng giải mã Swift JSONDecode không thành công nếu giải mã một phần tử không thành công


116

Trong khi sử dụng các giao thức Swift4 và Codable, tôi gặp sự cố sau - có vẻ như không có cách nào cho phép JSONDecoderbỏ qua các phần tử trong một mảng. Ví dụ: tôi có JSON sau:

[
    {
        "name": "Banana",
        "points": 200,
        "description": "A banana grown in Ecuador."
    },
    {
        "name": "Orange"
    }
]

Và một Codable struct:

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

Khi giải mã json này

let decoder = JSONDecoder()
let products = try decoder.decode([GroceryProduct].self, from: json)

Kết quả productslà trống. Điều này được mong đợi, do thực tế là đối tượng thứ hai trong JSON không có "points"khóa, trong khi pointskhông phải là tùy chọn trong GroceryProductcấu trúc.

Câu hỏi là làm thế nào tôi có thể cho phép JSONDecoder"bỏ qua" đối tượng không hợp lệ?


Chúng tôi không thể bỏ qua các đối tượng không hợp lệ nhưng bạn có thể gán các giá trị mặc định nếu nó là nil.
Vini App

1
Tại sao không thể pointschỉ khai báo tùy chọn?
NRitH

Câu trả lời:


115

Một tùy chọn là sử dụng loại trình bao bọc cố gắng giải mã một giá trị nhất định; lưu trữ nilnếu không thành công:

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

Sau đó, chúng tôi có thể giải mã một mảng trong số này, với việc bạn GroceryProductđiền vào Basetrình giữ chỗ:

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.")
//    )
// ]

Sau đó, chúng tôi đang sử dụng .compactMap { $0.base }để lọc ra nilcác phần tử (những phần tử gây ra lỗi khi giải mã).

Điều này sẽ tạo ra một mảng trung gian [FailableDecodable<GroceryProduct>], không phải là một vấn đề; tuy nhiên nếu bạn muốn tránh nó, bạn luôn có thể tạo một loại trình bao bọc khác để giải mã và mở từng phần tử từ một vùng chứa không có khóa:

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

Sau đó, bạn sẽ giải mã thành:

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
Điều gì sẽ xảy ra nếu đối tượng cơ sở không phải là một mảng, nhưng nó chứa một mảng? Thích {"products": [{"name": "banana" ...}, ...]}
ludvigeriksson,

2
@ludvigeriksson Bạn chỉ muốn thực hiện việc giải mã trong cấu trúc mà sau đó, ví dụ: gist.github.com/hamishknight/c6d270f7298e4db9e787aecb5b98bcae
Hamish

1
Swift's Codable thật dễ dàng, cho đến bây giờ .. điều này không thể đơn giản hơn được không?
Jonny

@Hamish Tôi không thấy bất kỳ xử lý lỗi nào cho dòng này. Điều gì xảy ra nếu một lỗi được ném vào đâyvar container = try decoder.unkeyedContainer()
bibscy

@bibscy Nó nằm trong phần thân của init(from:) throws, vì vậy Swift sẽ tự động truyền lỗi trở lại người gọi (trong trường hợp này là bộ giải mã, sẽ truyền lỗi trở lại JSONDecoder.decode(_:from:)cuộc gọi).
Hamish

33

Tôi sẽ tạo một kiểu mới Throwable, có thể bọc bất kỳ kiểu nào tuân theo 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)
        }
    }
}

Để giải mã một mảng GroceryProduct(hoặc bất kỳ mảng nào khác Collection):

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

thuộc valuetính tính toán được giới thiệu trong tiện ích mở rộng ở đâu trên Throwable:

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

Tôi sẽ chọn sử dụng enumloại trình bao bọc (trên a Struct) vì nó có thể hữu ích để theo dõi các lỗi được đưa ra cũng như các chỉ số của chúng.

Swift 5

Đối với Swift 5, hãy cân nhắc sử dụng Result enum ví dụ

struct Throwable<T: Decodable>: Decodable {
    let result: Result<T, Error>

    init(from decoder: Decoder) throws {
        result = Result(catching: { try T(from: decoder) })
    }
}

Để mở gói giá trị đã giải mã, hãy sử dụng get()phương thức trên thuộc resulttính:

let products = throwables.compactMap { try? $0.result.get() }

Tôi giống như câu trả lời này bởi vì tôi không phải lo lắng về cách viết bất kỳ tùy chỉnhinit
Mihai Fratu

Đây là giải pháp mà tôi đang tìm kiếm. Nó rất sạch sẽ và đơn giản. Cảm ơn vì điều này!
naturaln0va

24

Vấn đề là khi lặp qua một vùng chứa, containerner.currentIndex không được tăng lên nên bạn có thể thử giải mã lại bằng một kiểu khác.

Bởi vì currentIndex chỉ được đọc, giải pháp là tự gia tăng nó để giải mã thành công một giả. Tôi đã sử dụng giải pháp @Hamish và viết một trình bao bọc với một init tùy chỉnh.

Sự cố này là một lỗi Swift hiện tại: https://bugs.swift.org/browse/SR-5953

Giải pháp được đăng ở đây là một cách giải quyết trong một trong các nhận xét. Tôi thích tùy chọn này vì tôi đang phân tích cú pháp một loạt các mô hình theo cách giống nhau trên một máy khách mạng và tôi muốn giải pháp là cục bộ cho một trong các đối tượng. Đó là, tôi vẫn muốn những người khác bị loại bỏ.

Tôi giải thích rõ hơn trong github của mình 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
Một biến thể, thay vì một biến thể, if/elsetôi sử dụng một do/catchbên trong whilevòng lặp để tôi có thể ghi lại lỗi
Fraser

2
Câu trả lời này đề cập đến trình theo dõi lỗi Swift và có cấu trúc bổ sung đơn giản nhất (không có số liệu chung!), Vì vậy tôi nghĩ nó nên được chấp nhận.
Alper

2
Đây phải là câu trả lời được chấp nhận. Bất kỳ câu trả lời nào làm hỏng mô hình dữ liệu của bạn là một sự đánh đổi không thể chấp nhận được.
Joe Susnick

21

Có hai lựa chọn:

  1. Khai báo tất cả các thành viên của cấu trúc là tùy chọn có khóa có thể bị thiếu

    struct GroceryProduct: Codable {
        var name: String
        var points : Int?
        var description: String?
    }
  2. Viết trình khởi tạo tùy chỉnh để gán các giá trị mặc định trong niltrường hợp.

    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
Thay vì try?với decode, tốt hơn là sử dụng tryvới decodeIfPresenttrong tùy chọn thứ hai. Chúng ta chỉ cần đặt giá trị mặc định nếu không có khóa, không phải trong trường hợp không giải mã được, như khi khóa tồn tại, nhưng loại bị sai.
user28434 22/09/17

hey @vadian bạn có biết bất kỳ câu hỏi SO nào khác liên quan đến trình khởi tạo tùy chỉnh để gán giá trị mặc định trong trường hợp loại không khớp không? Tôi có một khóa là Int nhưng đôi khi sẽ là Chuỗi trong JSON vì vậy tôi đã thử làm theo những gì bạn đã nói ở trên, deviceName = try values.decodeIfPresent(Int.self, forKey: .deviceName) ?? 00000vì vậy nếu nó không thành công, nó sẽ chỉ đưa 0000 vào nhưng nó vẫn không thành công.
Martheli

Trong trường hợp decodeIfPresentnày là sai APIvì khóa không tồn tại. Sử dụng một do - catchkhối khác . Giải mã String, nếu một lỗi xảy ra, giải mãInt
vadian

13

Một giải pháp khả thi bởi Swift 5.1, sử dụng trình bao bọc thuộc tính:

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

Và sau đó là cách sử dụng:

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.

Lưu ý: Mọi thứ của trình bao bọc thuộc tính sẽ chỉ hoạt động nếu phản hồi có thể được bao bọc trong một cấu trúc (nghĩa là: không phải là một mảng cấp cao nhất). Trong trường hợp đó, bạn vẫn có thể bọc nó theo cách thủ công (với kiểu chữ để dễ đọc hơn):

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

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

7

Ive đã đưa giải pháp @ sophy-swicz, với một số sửa đổi, thành một tiện ích mở rộng dễ sử dụng

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

Chỉ cần gọi nó như thế này

init(from decoder: Decoder) throws {

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

    self.items = try container.decodeArray(ItemType.self, forKey: . items)
}

Đối với ví dụ trên:

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)

Ive đã gói giải pháp này trong một tiện ích mở rộng github.com/IdleHandsApps/SafeDecoder
Fraser

3

Rất tiếc, API Swift 4 không có bộ khởi tạo khả dụng cho init(from: Decoder) .

Chỉ có một giải pháp mà tôi thấy đang triển khai giải mã tùy chỉnh, cung cấp giá trị mặc định cho các trường tùy chọn và bộ lọc có thể có với dữ liệu cần thiết:

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

2

Tôi đã gặp vấn đề tương tự gần đây, nhưng hơi khác.

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

Trong trường hợp này, nếu một trong các phần tử trong friendnamesArraylà nil, thì toàn bộ đối tượng sẽ là nil trong khi giải mã.

Và cách đúng để xử lý trường hợp cạnh này là khai báo mảng chuỗi [String]là mảng các chuỗi tùy chọn [String?]như bên dưới,

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

2

Tôi đã cải thiện trên @ Hamish's cho trường hợp, bạn muốn hành vi này cho tất cả các mảng:

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

1

Câu trả lời của @ Hamish thật tuyệt. Tuy nhiên, bạn có thể giảm FailableCodableArrayxuống:

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

1

Thay vào đó, bạn cũng có thể làm như sau:

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

và sau đó trong khi nhận được nó:

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

0

Tôi nghĩ ra cái này KeyedDecodingContainer.safelyDecodeArraycung cấp một giao diện đơn giản:

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

Vòng lặp vô hạn tiềm ẩn while !container.isAtEndlà một mối quan tâm và nó được giải quyết bằng cách sử dụng EmptyDecodable.


0

Một nỗ lực đơn giản hơn nhiều: Tại sao bạn không khai báo các điểm là tùy chọn hoặc làm cho mảng chứa các phần tử tùy chọn

let products = [GroceryProduct?]
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.