web-dev-qa-db-fra.com

Les tableaux de décodage Swift JSONDecode échouent en cas d'échec du décodage d'un seul élément

En utilisant les protocoles Swift4 et Codable, j'ai eu le problème suivant: il semble qu'il n'y ait aucun moyen d'autoriser JSONDecoder à ignorer des éléments d'un tableau . Par exemple, j'ai le code JSON suivant:

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

Et un Codable struct:

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)

products résultant est vide. Ce qui est à prévoir du fait que le deuxième objet de JSON n'a pas de clé "points", alors que points n'est pas facultatif dans la structure GroceryProduct.

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

59
Khriapin Dmitriy

Une option consiste à utiliser un type de wrapper qui tente de décoder une valeur donnée; stocker nil en cas 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, avec votre GroceryProduct en remplissant l’espace réservé Base:

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 éléments nil (ceux qui ont généré une erreur lors du décodage).

Cela créera un tableau intermédiaire de [FailableDecodable<GroceryProduct>], qui ne devrait pas être un problème; Toutefois, si vous souhaitez l'éviter, vous pouvez toujours créer un autre type d'encapsuleur qui décode et décompresse 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.")
//    )
// ]
66
Hamish

Il y a deux options:

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

    struct GroceryProduct: Codable {
        var name: String
        var points : Int?
        var description: String?
    }
    
  2. Ecrivez un initialiseur personnalisé pour affecter les valeurs par défaut dans le cas nil.

    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

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

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

Ce problème est un bug courant 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 de nombreux modèles de la même manière sur un client réseau et je voulais que la solution soit locale par rapport à l'un des objets. . C'est-à-dire que je veux toujours que les autres soient jeté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)
14
Sophy Swicz

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 un type d'emballage enum (plutôt que Struct) car il peut être utile de garder une trace des erreurs générées ainsi que de leurs index.

8
cfergie

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

Il suffit d'appeler 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)
6
Fraser

Malheureusement, Swift 4 API n’a pas d’initialisateur disponible pour init(from: Decoder).

Une seule solution que je vois implémente un décodage personnalisé, donnant la valeur par défaut pour les champs optionnels 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)")
}
2
dimpiax

@ La réponse de Hamish est géniale. 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)
    }
}
1
Rob

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 dans friendnamesArray est nul, l'objet entier est nul lors du décodage.

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

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

Je viens avec ce KeyedDecodingContainer.safelyDecodeArray 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.isAtEnd est une préoccupation, et elle est résolue à l'aide de EmptyDecodable.

0
Haoxin Li

Une tentative beaucoup plus simple: pourquoi ne déclarez-vous pas les points comme optionnels ou ne faites-vous pas que le tableau contienne des éléments optionnels

let products = [GroceryProduct?]
0
BobbelKL