Array decoding Swift JSONDecode gagal jika decoding elemen tunggal gagal

116

Saat menggunakan protokol Swift4 dan Codable, saya mendapat masalah berikut - sepertinya tidak ada cara untuk mengizinkan JSONDecodermelewatkan elemen dalam array. Misalnya, saya memiliki JSON berikut:

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

Dan struct Codable :

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

Saat mendekode json ini

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

Hasil productskosong. Yang diharapkan, karena fakta bahwa objek kedua di JSON tidak memiliki "points"kunci, sedangkan struct pointsopsional GroceryProduct.

Pertanyaannya adalah bagaimana saya mengizinkan JSONDecoderuntuk "melewati" objek yang tidak valid?

Khriapin Dmitriy
sumber
Kami tidak dapat melewatkan objek yang tidak valid tetapi Anda dapat menetapkan nilai default jika nihil.
Aplikasi Vini
1
Mengapa tidak pointsbisa dinyatakan opsional?
NRitH

Jawaban:

115

Salah satu opsinya adalah menggunakan jenis pembungkus yang mencoba memecahkan kode nilai yang diberikan; menyimpan niljika tidak berhasil:

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

Kami kemudian dapat memecahkan kode array ini, dengan GroceryProductpengisian Anda di Baseplaceholder:

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

Kami kemudian menggunakan .compactMap { $0.base }untuk menyaringnil elemen (yang membuat kesalahan saat decoding).

Ini akan membuat array perantara [FailableDecodable<GroceryProduct>], yang seharusnya tidak menjadi masalah; namun jika Anda ingin menghindarinya, Anda selalu dapat membuat jenis pembungkus lain yang mendekode dan membuka bungkus setiap elemen dari wadah yang tidak dikunci:

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

Anda kemudian akan memecahkan kode sebagai:

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.")
//    )
// ]
Hamish
sumber
1
Bagaimana jika objek dasar bukan array, tetapi berisi satu? Seperti {"produk": [{"name": "banana" ...}, ...]}
ludvigeriksson
2
@ludvigeriksson Anda hanya ingin melakukan decoding dalam struktur itu, misalnya: gist.github.com/hamishknight/c6d270f7298e4db9e787aecb5b98bcae
Hamish
1
Swift's Codable mudah, sampai sekarang .. tidak bisakah ini dibuat lebih sederhana?
Jonny
@Hamish Saya tidak melihat kesalahan penanganan untuk baris ini. Apa yang terjadi jika kesalahan dilemparkan di sinivar container = try decoder.unkeyedContainer()
bibscy
@bibscy Ini ada di dalam tubuh init(from:) throws, jadi Swift akan secara otomatis menyebarkan kesalahan kembali ke pemanggil (dalam hal ini decoder, yang akan menyebarkannya kembali ke JSONDecoder.decode(_:from:)panggilan).
Hamish
34

Saya akan membuat tipe baru Throwable, yang dapat membungkus semua tipe yang sesuai dengan 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)
        }
    }
}

Untuk mendekode larik GroceryProduct(atau lainnya Collection):

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

di mana valueproperti yang dihitung diperkenalkan dalam ekstensi pada Throwable:

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

Saya akan memilih untuk menggunakan enumtipe pembungkus (lebih dari a Struct) karena mungkin berguna untuk melacak kesalahan yang dilemparkan serta indeksnya.

Cepat 5

Untuk Swift 5 Pertimbangkan untuk menggunakan misResult enum

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

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

Untuk membuka bungkus nilai yang didekodekan, gunakan get()metode pada resultproperti:

let products = throwables.compactMap { try? $0.result.get() }
cfergie
sumber
Saya suka jawaban ini karena saya tidak perlu khawatir menulis adat apa puninit
Mihai Fratu
Ini adalah solusi yang saya cari. Ini sangat bersih dan mudah. Terima kasih untuk ini!
naturaln0va
24

Masalahnya adalah saat melakukan iterasi pada sebuah container, container.currentIndex tidak bertambah sehingga Anda dapat mencoba mendekode lagi dengan tipe yang berbeda.

Karena currentIndex hanya dapat dibaca, solusinya adalah meningkatkannya sendiri dengan berhasil mendekode dummy. Saya mengambil solusi @Hamish, dan menulis pembungkus dengan init khusus.

Masalah ini adalah bug Swift saat ini: https://bugs.swift.org/browse/SR-5953

Solusi yang diposting di sini adalah solusi di salah satu komentar. Saya suka opsi ini karena saya mem-parsing banyak model dengan cara yang sama pada klien jaringan, dan saya ingin solusinya menjadi lokal untuk salah satu objek. Artinya, saya masih ingin yang lainnya dibuang.

Saya menjelaskan lebih baik di github saya 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)
Sophy Swicz
sumber
1
Satu variasi, alih-alih yang if/elsesaya gunakan do/catchdi dalam whileloop sehingga saya dapat mencatat kesalahan
Fraser
2
Jawaban ini menyebutkan pelacak bug Swift dan memiliki struct tambahan paling sederhana (tidak ada generik!) Jadi saya pikir itu harus diterima.
Alper
2
Ini harus menjadi jawaban yang diterima. Jawaban apa pun yang merusak model data Anda adalah imo tradeoff yang tidak dapat diterima.
Joe Susnick
21

Ada dua pilihan:

  1. Deklarasikan semua anggota struct sebagai opsional yang kuncinya bisa hilang

    struct GroceryProduct: Codable {
        var name: String
        var points : Int?
        var description: String?
    }
  2. Tulis penginisialisasi kustom untuk menetapkan nilai default dalam nilkasus tersebut.

    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) ?? ""
        }
    }
vadian
sumber
5
Daripada try?dengan decodeitu lebih baik digunakan trydengan decodeIfPresentdi opsi kedua. Kita perlu menyetel nilai default hanya jika tidak ada kunci, tidak jika terjadi kegagalan decoding, seperti saat kunci ada, tetapi jenisnya salah.
pengguna28434
hai @vadian apakah Anda tahu pertanyaan SO lain yang melibatkan penginisialisasi kustom untuk menetapkan nilai default jika jenis kasus tidak cocok? Saya memiliki kunci yang merupakan Int tetapi kadang-kadang akan menjadi String di JSON jadi saya mencoba melakukan apa yang Anda katakan di atas deviceName = try values.decodeIfPresent(Int.self, forKey: .deviceName) ?? 00000jadi jika gagal itu hanya akan memasukkan 0000 tetapi masih gagal.
Martheli
Dalam hal decodeIfPresentini salah APIkarena kuncinya memang ada. Gunakan do - catchblok lain . Dekode String, jika terjadi kesalahan, dekodeInt
vadian
13

Solusi yang dimungkinkan oleh Swift 5.1, menggunakan pembungkus properti:

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

Dan kemudian penggunaan:

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.

Catatan: Properti wrapper hanya akan berfungsi jika responsnya bisa dibungkus dalam struct (yaitu: bukan array tingkat atas). Dalam hal ini, Anda masih bisa membungkusnya secara manual (dengan typealias agar lebih mudah dibaca):

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

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

Saya telah menempatkan solusi @ sophy-swicz, dengan beberapa modifikasi, menjadi ekstensi yang mudah digunakan

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

Sebut saja seperti ini

init(from decoder: Decoder) throws {

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

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

Contoh di atas:

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)
Fraser
sumber
Saya telah membungkus solusi ini dalam ekstensi github.com/IdleHandsApps/SafeDecoder
Fraser
3

Sayangnya Swift 4 API tidak memiliki penginisialisasi yang gagal untuk init(from: Decoder) .

Hanya satu solusi yang saya lihat adalah mengimplementasikan decoding kustom, memberikan nilai default untuk kolom opsional dan kemungkinan filter dengan data yang diperlukan:

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)")
}
dimpiax
sumber
2

Saya mengalami masalah serupa baru-baru ini, tetapi sedikit berbeda.

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

Dalam hal ini, jika salah satu elemen di friendnamesArrayadalah nihil, seluruh objek nihil saat decoding.

Dan cara yang tepat untuk menangani kasus tepi ini adalah dengan mendeklarasikan array string [String]sebagai array string opsional [String?]seperti di bawah ini,

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

Saya meningkatkan @ Hamish untuk kasus ini, bahwa Anda menginginkan perilaku ini untuk semua array:

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
    }
}
Sören Schmaljohann
sumber
1

@ Jawaban Hamish bagus. Namun, Anda dapat mengurangi FailableCodableArraymenjadi:

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)
    }
}
Robert Crabtree
sumber
1

Sebagai gantinya, Anda juga bisa melakukan seperti ini:

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

dan kemudian saat mendapatkannya:

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

Saya datang dengan ini KeyedDecodingContainer.safelyDecodeArrayyang menyediakan antarmuka sederhana:

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

Perulangan yang berpotensi tak terbatas while !container.isAtEndmenjadi perhatian, dan ditangani dengan menggunakan EmptyDecodable.

Haoxin Li
sumber
0

Upaya yang jauh lebih sederhana: Mengapa Anda tidak mendeklarasikan poin sebagai opsional atau membuat array berisi elemen opsional

let products = [GroceryProduct?]
BobbelKL
sumber