it-swarm.com.de

Swift-JSONDecode-Decodierungsarrays schlagen fehl, wenn die Decodierung einzelner Elemente fehlschlägt

Bei der Verwendung von Swift4- und Codable-Protokollen trat folgendes Problem auf: Es scheint nicht möglich zu sein, dass JSONDecoder Elemente in einem Array überspringen kann . Zum Beispiel habe ich die folgende JSON:

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

Und ein Codierbar struct:

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

Bei der Dekodierung dieses Json

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

Ergebnis products ist leer. Dies ist zu erwarten, da das zweite Objekt in JSON keinen "points"-Schlüssel hat, während points in GroceryProduct struct nicht optional ist.

Die Frage ist, wie kann ich zulassen, dass JSONDecoder ein ungültiges Objekt "überspringt"?

59

Eine Option ist die Verwendung eines Wrapper-Typs, der versucht, einen bestimmten Wert zu decodieren. Speichern von nil falls 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, wobei Ihre GroceryProduct den Base-Platzhalter ausfüllt:

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 nil-Elemente herauszufiltern (diejenigen, die einen Fehler beim Dekodieren ausgelöst haben).

Dadurch wird ein Zwischenarray von [FailableDecodable<GroceryProduct>] erstellt, das kein Problem sein sollte. Wenn Sie es jedoch vermeiden möchten, können Sie immer einen anderen Wrapper-Typ erstellen, der jedes Element aus einem Container ohne Schlüssel 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.")
//    )
// ]
66
Hamish

Es gibt zwei Möglichkeiten:

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

    struct GroceryProduct: Codable {
        var name: String
        var points : Int?
        var description: String?
    }
    
  2. Schreiben Sie einen benutzerdefinierten Initialisierer, um im Fall nil 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) ?? ""
        }
    }
    
15
vadian

Das Problem ist, dass der Container.currentIndex beim Iterieren über einen Container nicht inkrementiert wird, sodass Sie erneut mit einem anderen Typ dekodieren können. 

Da der currentIndex schreibgeschützt ist, können Sie ihn inkrementieren, wenn Sie einen Dummy erfolgreich 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 auf einem Netzwerkclient eine Reihe von Modellen auf dieselbe Weise parse und die Lösung lokal für eines der Objekte sein sollte . Das heißt, ich möchte immer noch, dass die anderen weggeworfen 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)
14
Sophy Swicz

Ich würde einen neuen Typ Throwable erstellen, der jeden Typ umwandeln kann, der Decodable entspricht:

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 einer beliebigen anderen Collection):

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

dabei ist value eine berechnete Eigenschaft, die in einer Erweiterung von Throwable eingeführt wurde:

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

Ich würde mich für einen enum-Wrapper-Typ (über eine Struct) entscheiden, da es nützlich sein kann, die Fehler, die ausgelöst werden, sowie deren Indizes zu verfolgen.

8
cfergie

Ich habe die @ sophy-swicz-Lösung mit einigen Modifikationen in eine einfach zu verwendende Erweiterung gestellt

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)
6
Fraser

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

Nur eine der Lösungen, die ich sehe, ist die Implementierung einer benutzerdefinierten Dekodierung, die Standardwerte für optionale Felder und einen möglichen Filter mit 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
dimpiax

@ Hamishs Antwort ist großartig. Sie können jedoch FailableCodableArray auf Folgendes reduzieren:

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
Rob

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 ist, wenn eines der Elemente in friendnamesArray Null ist, das gesamte Objekt beim Dekodieren Null.

Die richtige Vorgehensweise für diesen Edge-Fall besteht darin, den String array[String] als Array optionaler Strings[String?] zu deklarieren (siehe unten).

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

Ich komme mit diesem KeyedDecodingContainer.safelyDecodeArray, der eine einfache Schnittstelle 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.isAtEnd ist ein Problem und wird mit EmptyDecodable adressiert.

0
Haoxin Li

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

let products = [GroceryProduct?]
0
BobbelKL