Gli array di decodifica Swift JSONDecode falliscono se la decodifica di un singolo elemento fallisce


116

Durante l'utilizzo dei protocolli Swift4 e Codable ho riscontrato il seguente problema: sembra che non ci sia modo di consentire JSONDecoderdi saltare gli elementi in un array. Ad esempio, ho il seguente JSON:

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

E una struttura Codable :

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

Durante la decodifica di questo file json

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

Il risultato productsè vuoto. Il che è prevedibile, dato che il secondo oggetto in JSON non ha "points"chiave, mentre pointsnon è opzionale in GroceryProductstruct.

La domanda è: come posso consentire JSONDecoderdi "saltare" un oggetto non valido?


Non possiamo saltare gli oggetti non validi ma puoi assegnare valori predefiniti se è nullo.
App Vini

1
Perché non può essere pointssemplicemente dichiarato facoltativo?
NRitH

Risposte:


115

Un'opzione è usare un tipo wrapper che tenta di decodificare un dato valore; memorizzazione in nilcaso di esito negativo:

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

Possiamo quindi decodificare un array di questi, GroceryProductinserendo il Basesegnaposto:

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

Stiamo quindi utilizzando .compactMap { $0.base }per filtrare gli nilelementi (quelli che hanno generato un errore di decodifica).

Questo creerà un array intermedio di [FailableDecodable<GroceryProduct>], che non dovrebbe essere un problema; tuttavia, se desideri evitarlo, puoi sempre creare un altro tipo di wrapper che decodifica e scarta ogni elemento da un contenitore senza chiave:

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

Dovresti quindi decodificare come:

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
E se l'oggetto base non è un array, ma ne contiene uno? Mi piace {"prodotti": [{"name": "banana" ...}, ...]}
ludvigeriksson

2
@ludvigeriksson Vuoi solo eseguire la decodifica all'interno di quella struttura, quindi, ad esempio: gist.github.com/hamishknight/c6d270f7298e4db9e787aecb5b98bcae
Hamish

1
Codable di Swift è stato facile, fino ad ora .. non può essere reso un po 'più semplice?
Jonny

@ Hamish Non vedo alcuna gestione degli errori per questa riga. Cosa succede se viene generato un errore quivar container = try decoder.unkeyedContainer()
bibscy

@bibscy È all'interno del corpo di init(from:) throws, quindi Swift propagherà automaticamente l'errore al chiamante (in questo caso il decodificatore, che lo propagherà alla JSONDecoder.decode(_:from:)chiamata).
Hamish

34

Creerei un nuovo tipo Throwable, che può avvolgere qualsiasi tipo conforme a 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)
        }
    }
}

Per decodificare un array di GroceryProduct(o qualsiasi altro Collection):

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

dove valueè una proprietà calcolata introdotta in un'estensione su Throwable:

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

Opterei per l'utilizzo di un enumtipo wrapper (su a Struct) perché può essere utile per tenere traccia degli errori che vengono lanciati così come i loro indici.

Swift 5

Per Swift 5 Considerare l'utilizzo dell'esResult enum

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

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

Per scartare il valore decodificato utilizzare il get()metodo sulla resultproprietà:

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

Mi piace questa risposta perché non devo preoccuparmi di scrivere alcuna custominit
Mihai Fratu

Questa è la soluzione che stavo cercando. È così pulito e semplice. Grazie per questo!
naturaln0va

24

Il problema è che durante l'iterazione su un contenitore, container.currentIndex non viene incrementato, quindi puoi provare a decodificare di nuovo con un tipo diverso.

Poiché currentIndex è di sola lettura, una soluzione è incrementarlo da soli decodificando con successo un dummy. Ho preso la soluzione @Hamish e ho scritto un wrapper con un init personalizzato.

Questo problema è un bug corrente di Swift: https://bugs.swift.org/browse/SR-5953

La soluzione pubblicata qui è una soluzione alternativa in uno dei commenti. Mi piace questa opzione perché sto analizzando un gruppo di modelli allo stesso modo su un client di rete e volevo che la soluzione fosse locale per uno degli oggetti. Cioè, voglio ancora che gli altri vengano scartati.

Spiego meglio nel mio 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
Una variazione, invece di if/elseutilizzare una do/catchall'interno del whileciclo in modo da poter registrare l'errore
Fraser

2
Questa risposta menziona il bug tracker di Swift e ha la struttura aggiuntiva più semplice (senza generici!) Quindi penso che dovrebbe essere quella accettata.
Alper

2
Questa dovrebbe essere la risposta accettata. Qualsiasi risposta che corrompe il tuo modello di dati è un compromesso inaccettabile imo.
Joe Susnick

21

Ci sono due opzioni:

  1. Dichiarare tutti i membri della struttura come facoltativi le cui chiavi possono mancare

    struct GroceryProduct: Codable {
        var name: String
        var points : Int?
        var description: String?
    }
  2. Scrivi un inizializzatore personalizzato per assegnare valori predefiniti nel nilcaso.

    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
Invece che try?con decodeè meglio usare trycon decodeIfPresentnella seconda opzione. Dobbiamo impostare il valore predefinito solo se non c'è la chiave, non in caso di errori di decodifica, come quando la chiave esiste, ma il tipo è sbagliato.
user28434

hey @vadian conosci altre domande SO che coinvolgono l'inizializzatore personalizzato per assegnare valori predefiniti nel caso in cui il tipo non corrisponda? Ho una chiave che è un Int ma a volte sarà una stringa nel JSON, quindi ho provato a fare quello che hai detto sopra, deviceName = try values.decodeIfPresent(Int.self, forKey: .deviceName) ?? 00000quindi se fallisce inserirà 0000 ma fallisce ancora.
Martheli

In questo caso decodeIfPresentè sbagliato APIperché la chiave esiste. Usa un altro do - catchblocco. Decodifica String, se si verifica un errore, decodificaInt
vadian

13

Una soluzione resa possibile da Swift 5.1, utilizzando il property wrapper:

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

E poi l'utilizzo:

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.

Nota: le cose del wrapper della proprietà funzioneranno solo se la risposta può essere racchiusa in uno struct (cioè: non un array di primo livello). In tal caso, puoi comunque avvolgerlo manualmente (con un typealias per una migliore leggibilità):

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

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

7

Ho inserito la soluzione @ sophy-swicz, con alcune modifiche, in un'estensione facile da usare

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

Chiamalo così

init(from decoder: Decoder) throws {

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

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

Per l'esempio sopra:

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)

Ho racchiuso questa soluzione in un'estensione github.com/IdleHandsApps/SafeDecoder
Fraser

3

Sfortunatamente l'API di Swift 4 non ha un inizializzatore fallibile per init(from: Decoder).

Solo una soluzione che vedo è l'implementazione della decodifica personalizzata, fornendo un valore predefinito per i campi opzionali e un possibile filtro con i dati necessari:

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

Ho avuto un problema simile di recente, ma leggermente diverso.

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

In questo caso, se uno degli elementi in friendnamesArrayè nullo, l'intero oggetto è nullo durante la decodifica.

E il modo giusto per gestire questo caso limite è dichiarare l'array di stringhe [String]come array di stringhe opzionali [String?]come di seguito,

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

2

Ho migliorato @ Hamish per il caso, che vuoi questo comportamento per tutti gli 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
    }
}

1

La risposta di @ Hamish è fantastica. Tuttavia, puoi ridurre FailableCodableArraya:

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

Invece, puoi anche fare in questo modo:

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

e poi mentre lo ottieni:

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

0

Mi viene in mente questo KeyedDecodingContainer.safelyDecodeArrayche fornisce un'interfaccia semplice:

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

Il ciclo potenzialmente infinito while !container.isAtEndè un problema e viene risolto utilizzando EmptyDecodable.


0

Un tentativo molto più semplice: perché non dichiari i punti come opzionali o fai in modo che l'array contenga elementi opzionali

let products = [GroceryProduct?]
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.