Schnelle JSONDecode-Decodierungsarrays schlagen fehl, wenn die Einzelelementdecodierung fehlschlägt


116

Bei der Verwendung der Protokolle Swift4 und Codable trat das folgende Problem auf: Es gibt anscheinend keine Möglichkeit, das JSONDecoderÜberspringen von Elementen in einem Array zuzulassen . Zum Beispiel habe ich den folgenden JSON:

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

Und eine codierbare Struktur:

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

Beim Dekodieren dieses JSON

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

Das Ergebnis productsist leer. Dies ist zu erwarten, da das zweite Objekt in JSON keinen "points"Schlüssel hat, während pointses in GroceryProductstruct nicht optional ist .

Die Frage ist, wie ich zulassen kann, dass ein JSONDecoderungültiges Objekt "übersprungen" wird.


Wir können die ungültigen Objekte nicht überspringen, aber Sie können Standardwerte zuweisen, wenn sie Null sind.
Vini App

1
Warum kann nicht pointseinfach als optional deklariert werden?
NRitH

Antworten:


115

Eine Möglichkeit besteht darin, einen Wrapper-Typ zu verwenden, der versucht, einen bestimmten Wert zu dekodieren. Speichern, nilwenn nicht erfolgreich:

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

Wir können dann ein Array davon dekodieren, GroceryProductindem Sie den BasePlatzhalter ausfüllen :

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

Wir verwenden dann .compactMap { $0.base }, um nilElemente herauszufiltern (diejenigen, die beim Decodieren einen Fehler verursacht haben).

Dadurch wird ein Zwischenarray von erstellt [FailableDecodable<GroceryProduct>], das kein Problem darstellen sollte. Wenn Sie dies jedoch vermeiden möchten, können Sie jederzeit einen anderen Wrapper-Typ erstellen, der jedes Element aus einem nicht verschlüsselten Container dekodiert und entpackt:

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

Sie würden dann dekodieren als:

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
Was ist, wenn das Basisobjekt kein Array ist, aber eines enthält? Wie {"Produkte": [{"Name": "Banane" ...}, ...]}
ludvigeriksson

2
@ludvigeriksson Sie möchten dann nur die Dekodierung innerhalb dieser Struktur durchführen, zum Beispiel: gist.github.com/hamishknight/c6d270f7298e4db9e787aecb5b98bcae
Hamish

1
Swift's Codable war bis jetzt einfach. Kann das nicht ein bisschen einfacher gemacht werden?
Jonny

@ Hamish Ich sehe keine Fehlerbehandlung für diese Zeile. Was passiert, wenn hier ein Fehler ausgelöst wirdvar container = try decoder.unkeyedContainer()
bibscy

@bibscy Es befindet sich im Hauptteil von init(from:) throws, sodass Swift den Fehler automatisch an den Anrufer zurücksendet (in diesem Fall an den Decoder, der ihn an den JSONDecoder.decode(_:from:)Anruf zurückgibt ).
Hamish

33

Ich würde einen neuen Typ erstellen Throwable, der jeden Typ umschließen kann, der entspricht 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)
        }
    }
}

Zum Dekodieren eines Arrays von GroceryProduct(oder einem anderen Collection):

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

wobei valueeine berechnete Eigenschaft in einer Erweiterung eingeführt auf Throwable:

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

Ich würde mich für die Verwendung eines enumWrapper-Typs (über a Struct) entscheiden, da es nützlich sein kann, die ausgelösten Fehler sowie deren Indizes zu verfolgen.

Swift 5

Für Swift 5 Verwenden Sie zResult enum

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

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

Verwenden Sie die get()Methode für die resultEigenschaft, um den dekodierten Wert zu entpacken :

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

Ich mag diese Antwort, weil ich mir keine Sorgen machen muss, einen Brauch zu schreibeninit
Mihai Fratu

Dies ist die Lösung, nach der ich gesucht habe. Es ist so sauber und unkompliziert. Danke dafür!
naturaln0va

24

Das Problem ist, dass beim Durchlaufen eines Containers der container.currentIndex nicht inkrementiert wird, sodass Sie versuchen können, erneut mit einem anderen Typ zu dekodieren.

Da der aktuelle Index schreibgeschützt ist, besteht eine Lösung darin, ihn selbst zu erhöhen und einen Dummy erfolgreich zu dekodieren. Ich nahm die @ Hamish-Lösung und schrieb einen Wrapper mit einem benutzerdefinierten Init.

Dieses Problem ist ein aktueller Swift-Fehler: https://bugs.swift.org/browse/SR-5953

Die hier veröffentlichte Lösung ist eine Problemumgehung in einem der Kommentare. Diese Option gefällt mir, weil ich eine Reihe von Modellen auf die gleiche Weise auf einem Netzwerkclient analysiere und wollte, dass die Lösung für eines der Objekte lokal ist. Das heißt, ich möchte immer noch, dass die anderen verworfen werden.

Ich erkläre es besser in meinem 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
Eine Variante, anstatt einer, verwende if/elseich eine do/catchinnerhalb der whileSchleife, damit ich den Fehler protokollieren kann
Fraser

2
Diese Antwort erwähnt den Swift Bug Tracker und hat die einfachste zusätzliche Struktur (keine Generika!), Daher denke ich, dass es die akzeptierte sein sollte.
Alper

2
Dies sollte die akzeptierte Antwort sein. Jede Antwort, die Ihr Datenmodell beschädigt, ist ein inakzeptabler Kompromiss.
Joe Susnick

21

Es gibt zwei Möglichkeiten:

  1. Deklarieren Sie alle Mitglieder der Struktur als optional, deren Schlüssel fehlen können

    struct GroceryProduct: Codable {
        var name: String
        var points : Int?
        var description: String?
    }
  2. Schreiben Sie einen benutzerdefinierten Initialisierer, um in diesem nilFall Standardwerte zuzuweisen .

    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
Anstelle von try?mit ist decodees besser, trymit decodeIfPresentin der zweiten Option zu verwenden. Wir müssen den Standardwert nur festlegen, wenn kein Schlüssel vorhanden ist, nicht im Falle eines Dekodierungsfehlers, z. B. wenn ein Schlüssel vorhanden ist, der Typ jedoch falsch ist.
user28434

hey @vadian Kennen Sie weitere SO-Fragen zum benutzerdefinierten Initialisierer, um Standardwerte zuzuweisen, falls der Typ nicht übereinstimmt? Ich habe einen Schlüssel, der ein Int ist, aber manchmal ein String im JSON ist. Deshalb habe ich versucht, das zu tun, was Sie oben gesagt haben. deviceName = try values.decodeIfPresent(Int.self, forKey: .deviceName) ?? 00000Wenn dies fehlschlägt, wird nur 0000 eingegeben, aber es schlägt immer noch fehl.
Martheli

In diesem Fall decodeIfPresentist das falsch, APIweil der Schlüssel existiert. Verwenden Sie einen anderen do - catchBlock. Decode String, wenn ein Fehler auftritt, zu dekodierenInt
Vadian

13

Eine Lösung, die Swift 5.1 mithilfe des Eigenschafts-Wrappers ermöglicht:

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

Und dann die Verwendung:

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.

Hinweis: Der Property Wrapper funktioniert nur, wenn die Antwort in eine Struktur eingeschlossen werden kann (dh kein Array der obersten Ebene). In diesem Fall können Sie es weiterhin manuell umbrechen (mit einem Typealias zur besseren Lesbarkeit):

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

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

7

Ich habe die @ sophy-swicz-Lösung mit einigen Modifikationen in eine benutzerfreundliche Erweiterung integriert

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

Nennen Sie es einfach so

init(from decoder: Decoder) throws {

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

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

Für das obige Beispiel:

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)

Ich habe diese Lösung in einer Erweiterung github.com/IdleHandsApps/SafeDecoder
Fraser

3

Leider hat die Swift 4 API keinen fehlgeschlagenen Initialisierer für init(from: Decoder).

Ich sehe nur eine Lösung, die die benutzerdefinierte Dekodierung implementiert und einen Standardwert für optionale Felder und einen möglichen Filter mit den erforderlichen Daten angibt:

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

Ich hatte kürzlich ein ähnliches Problem, aber etwas anders.

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

In diesem Fall friendnamesArrayist das gesamte Objekt beim Decodieren Null , wenn eines der Elemente in Null ist.

Und der richtige Weg , diese Kante Fall zu behandeln , ist das String - Array zu deklarieren [String]als Array von optionalen Strings [String?]wie unten,

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

2

Ich habe @ Hamish's für den Fall verbessert, dass Sie dieses Verhalten für alle Arrays wünschen:

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

@ Hamishs Antwort ist großartig. Sie können jedoch Folgendes reduzieren 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)
    }
}

1

Stattdessen können Sie auch Folgendes tun:

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

und dann rein, während du es bekommst:

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

0

Ich habe mir das ausgedacht KeyedDecodingContainer.safelyDecodeArray, das eine einfache Oberfläche bietet:

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

Die potenziell unendliche Schleife while !container.isAtEndist ein Problem und wird mithilfe von behoben EmptyDecodable.


0

Ein viel einfacherer Versuch: Warum deklarieren Sie Punkte nicht als optional oder lassen das Array optionale Elemente enthalten?

let products = [GroceryProduct?]
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.