Les tableaux de décodage Swift jsondecode échouent si le décodage d'un seul élément échoue

En utilisant les protocoles Swift4 et Codables, j'ai eu le problème suivant : il semble qu'il n'y ait aucun moyen de permettre à JSONDecoder de sauter des éléments dans un tableau. Par exemple, j'ai JSON suivant:

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

Et une structureCodable :

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)

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

Question Est Comment puis-je autoriser JSONDecoder à "sauter" un objet invalide?

41
demandé sur Hamish 2017-09-21 16:16:03

7 réponses

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 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 alors .compactMap { $0.base } pour filtrer les éléments nil (ceux qui ont jeté une erreur sur le décodage).

Cela va créer 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 wrapper type qui décode et déballage chaque élément d'un conteneur Non Verrouillé:

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.")
//    )
// ]
49
répondu Hamish 2018-07-09 12:54:25

Il y a deux options:

  1. Déclare 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 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) ?? ""
        }
    }
    
14
répondu vadian 2017-12-05 09:35:12

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

Parce que le 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 Swift actuel: https://bugs.swift.org/browse/SR-5953

La solution affichée ici est une solution de contournement dans l'un des commentaires. J'aime cette option parce que 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 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)
13
répondu Sophy Swicz 2017-11-03 13:42:14

Ive a 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-le comme ceci

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)
5
répondu Fraser 2018-08-02 11:39:04

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 de toute 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 utiliser un type de wrapper enum (sur un Struct) car il peut être utile de garder une trace des erreurs qui sont lancées ainsi que de leurs indices.

4
répondu cfergie 2018-09-07 13:04:11

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

Une seule solution que je vois implémente un décodage personnalisé, donnant une 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
répondu dimpiax 2017-09-22 13:37:59

J'ai rencontré le même problème et n'ai trouvé aucune des réponses satisfaisante.

J'avais la structure suivante:

public struct OfferResponse {
public private(set) var offers: [Offer]

public init(data: Data) throws {
    let json = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: [Any]]
    guard let offersDataArray = json?["Offers"] else {
        throw NSError(domain: "unexpected JSON structure for \(type(of: self))", code: 36, userInfo: nil)
    }
    guard let firstOfferData = offersDataArray.first else {
        throw NSError(domain: "emptyArray in JSON structure for \(type(of: self))", code: 36, userInfo: nil)
    }
    let decoder = JSONDecoder()
    offers = try decoder.decode([Offer].self, from: JSONSerialization.data(withJSONObject: firstOfferData, options: .prettyPrinted))
}

À un moment donné, le backend a renvoyé un mauvais contenu pour un élément. Je l'ai résolu de cette façon:

    offers = []
    for offerData in offersDataArray {
        if let offer = try? decoder.decode(Offer.self, from: JSONSerialization.data(withJSONObject: offerData, options: .prettyPrinted)) {
            offers.append(offer)
        }            
-2
répondu BobbelKL 2018-06-19 18:22:17