Les matrices de décodage Swift JSONDecode échouent si le décodage d'un seul élément échoue

116

Lors de l'utilisation des protocoles Swift4 et Codable, j'ai eu le problème suivant - il semble qu'il n'y ait aucun moyen d'autoriser JSONDecoderle saut d'éléments dans un tableau. Par exemple, j'ai le JSON suivant:

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

Et une structure codable :

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

Lors du décodage de ce json

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

Le résultat productsest vide. Ce qui est normal, du fait que le deuxième objet dans JSON n'a pas de "points"clé, alors qu'il pointsn'est pas facultatif dans GroceryProductstruct.

La question est de savoir comment puis-je autoriser JSONDecoder"ignorer" un objet invalide?

Khriapin Dmitriy
la source
Nous ne pouvons pas ignorer les objets non valides, mais vous pouvez attribuer des valeurs par défaut si elles sont nulles.
Vini App
1
Pourquoi ne peut-on pas pointssimplement être déclaré facultatif?
NRitH

Réponses:

115

Une option consiste à utiliser un type de wrapper qui tente de décoder une valeur donnée; stockage en nilcas d'échec:

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

Nous pouvons ensuite décoder un tableau de ceux-ci, en GroceryProductremplissant l' Baseespace réservé:

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

Nous utilisons ensuite .compactMap { $0.base }pour filtrer les niléléments (ceux qui ont généré une erreur de décodage).

Cela créera un tableau intermédiaire de [FailableDecodable<GroceryProduct>], ce qui ne devrait pas être un problème; cependant, si vous souhaitez l'éviter, vous pouvez toujours créer un autre type de wrapper qui décode et déballe chaque élément d'un conteneur sans clé:

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

Vous décoderiez alors comme:

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
la source
1
Que faire si l'objet de base n'est pas un tableau, mais qu'il en contient un? J'aime {"products": [{"name": "banana" ...}, ...]}
ludvigeriksson
2
@ludvigeriksson Vous voulez simplement effectuer le décodage dans cette structure, par exemple: gist.github.com/hamishknight/c6d270f7298e4db9e787aecb5b98bcae
Hamish
1
Le codable de Swift était facile, jusqu'à présent ... cela ne peut-il pas être un peu plus simple?
Jonny
@Hamish Je ne vois aucune gestion des erreurs pour cette ligne. Que se passe-t-il si une erreur est lancée icivar container = try decoder.unkeyedContainer()
bibscy
@bibscy C'est dans le corps de init(from:) throws, donc Swift propagera automatiquement l'erreur à l'appelant (dans ce cas, le décodeur, qui la propagera à l' JSONDecoder.decode(_:from:)appel).
Hamish
34

Je créerais un nouveau type Throwable , qui peut envelopper tout type conforme à 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)
        }
    }
}

Pour décoder un tableau de GroceryProduct (ou tout autre Collection):

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

value est une propriété calculée introduite dans une extension sur Throwable:

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

J'opterais pour l'utilisation d'un enumtype wrapper (sur a Struct) car il peut être utile de garder une trace des erreurs qui sont levées ainsi que de leurs index.

Swift 5

Pour Swift 5 Pensez à utiliser par exempleResult enum

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

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

Pour dérouler la valeur décodée, utilisez la get()méthode sur la resultpropriété:

let products = throwables.compactMap { try? $0.result.get() }
cfergie
la source
J'aime cette réponse parce que je n'ai pas à me soucier d'écrire une personnalisationinit
Mihai Fratu
C'est la solution que je recherchais. C'est tellement propre et simple. Merci pour ça!
naturaln0va le
24

Le problème est que lors de l'itération sur un conteneur, le container.currentIndex n'est pas incrémenté afin que vous puissiez essayer de décoder à nouveau avec un type différent.

Étant donné que currentIndex est en lecture seule, une solution consiste à l'incrémenter vous-même en décodant avec succès un mannequin. J'ai pris la solution @Hamish et j'ai écrit un wrapper avec un init personnalisé.

Ce problème est un bug actuel de Swift: https://bugs.swift.org/browse/SR-5953

La solution publiée ici est une solution de contournement dans l'un des commentaires. J'aime cette option car j'analyse un tas de modèles de la même manière sur un client réseau, et je voulais que la solution soit locale à l'un des objets. Autrement dit, je veux toujours que les autres soient écartés.

J'explique mieux dans mon 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)
Sophy Swicz
la source
1
Une variante, au lieu d'un if/elsej'utilise un do/catchà l'intérieur de la whileboucle pour que je puisse enregistrer l'erreur
Fraser
2
Cette réponse mentionne le suivi des bogues Swift et a la structure supplémentaire la plus simple (pas de génériques!) Donc je pense que cela devrait être celui accepté.
Alper le
2
Cela devrait être la réponse acceptée. Toute réponse qui corrompt votre modèle de données est un compromis inacceptable.
Joe Susnick
21

Il existe deux options:

  1. Déclarez tous les membres de la structure comme facultatifs dont les clés peuvent être manquantes

    struct GroceryProduct: Codable {
        var name: String
        var points : Int?
        var description: String?
    }
  2. Écrivez un initialiseur personnalisé pour attribuer des valeurs par défaut au nilcas.

    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
la source
5
Au lieu de try?avec, decodeil est préférable d'utiliser tryavec decodeIfPresentdans la deuxième option. Nous devons définir la valeur par défaut uniquement s'il n'y a pas de clé, pas en cas d'échec du décodage, comme lorsque la clé existe, mais que le type est incorrect.
user28434
hey @vadian connaissez-vous d'autres questions SO impliquant un initialiseur personnalisé pour attribuer des valeurs par défaut au cas où le type de cas ne correspond pas? J'ai une clé qui est un Int mais qui sera parfois une chaîne dans le JSON, j'ai donc essayé de faire ce que vous avez dit ci-dessus, deviceName = try values.decodeIfPresent(Int.self, forKey: .deviceName) ?? 00000donc si cela échoue, il mettra simplement 0000 mais cela échoue toujours.
Martheli
Dans ce cas, decodeIfPresentc'est faux APIcar la clé existe. Utilisez un autre do - catchbloc. Décoder String, si une erreur se produit, décoderInt
vadian
13

Une solution rendue possible par Swift 5.1, en utilisant le wrapper de propriété:

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

Et puis l'utilisation:

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.

Remarque: le wrapper de propriétés ne fonctionnera que si la réponse peut être enveloppée dans une structure (c'est-à-dire: pas un tableau de niveau supérieur). Dans ce cas, vous pouvez toujours l'envelopper manuellement (avec un typealias pour une meilleure lisibilité):

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

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

J'ai mis la solution @ sophy-swicz, avec quelques modifications, dans une extension facile à utiliser

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

Appelez ça comme ça

init(from decoder: Decoder) throws {

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

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

Pour l'exemple ci-dessus:

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
la source
J'ai enveloppé cette solution dans une extension github.com/IdleHandsApps/SafeDecoder
Fraser
3

Malheureusement, l'API Swift 4 n'a pas d'initialiseur disponible pour init(from: Decoder) .

Une seule solution que je vois consiste à implémenter un décodage personnalisé, en donnant une valeur par défaut pour les champs facultatifs et un filtre possible avec les données nécessaires:

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
la source
2

J'ai eu un problème similaire récemment, mais légèrement différent.

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

Dans ce cas, si l'un des éléments de friendnamesArray est nul, l'objet entier est nul lors du décodage.

Et la bonne façon de gérer ce cas de bord est de déclarer le [String]tableau de chaînes en [String?]tant que tableau de chaînes facultatives comme ci-dessous,

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

J'ai amélioré @ Hamish's pour le cas, que vous voulez ce comportement pour tous les tableaux:

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
la source
1

La réponse de @ Hamish est excellente. Cependant, vous pouvez réduire 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)
    }
}
Robert Crabtree
la source
1

Au lieu de cela, vous pouvez également faire comme ceci:

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

puis en le récupérant:

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

Je KeyedDecodingContainer.safelyDecodeArraypropose ceci qui fournit une interface simple:

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

La boucle potentiellement infinie while !container.isAtEndest un problème, et elle est résolue en utilisant EmptyDecodable.

Haoxin Li
la source
0

Une tentative beaucoup plus simple: pourquoi ne pas déclarer les points comme facultatifs ou faire en sorte que le tableau contienne des éléments facultatifs

let products = [GroceryProduct?]
BobbelKL
la source