158 votes

Swift JSONDecode décodant les tableaux échoue si le décodage d'un seul élément échoue

Lors de l'utilisation des protocoles Swift4 et Codable, j'ai rencontré le problème suivant - il semble qu'il n'y ait pas de moyen de permettre à JSONDecoder de sauter des éléments dans un tableau. Par exemple, j'ai le JSON suivant :

[
    {
        "name": "Banane",
        "points": 200,
        "description": "Une banane cultivée en Équateur."
    },
    {
        "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 products résultant est vide. Ce qui est normal, étant donné que le deuxième objet dans le JSON n'a pas de clé "points", alors que points n'est pas optionnel dans la structure GroceryProduct.

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

0 votes

Nous ne pouvons pas ignorer les objets invalides, mais vous pouvez leur attribuer des valeurs par défaut s'ils sont nuls.

3 votes

Pourquoi ne peut pas points juste être déclaré optionnel?

0 votes

Parce que parfois un champ manquant n'a pas de sens, et le rendre optionnel ruinerait votre modèle.

157voto

Hamish Points 42073

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

struct FailableDecodable : 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 ces éléments, avec votre GroceryProduct remplissant l'espace réservé Base :

import Foundation

let json = """
[
    {
        "name": "Banana",
        "points": 200,
        "description": "Une banane cultivée en Équateur."
    },
    {
        "name": "Orange"
    }
]
""".data(using: .utf8)!

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

let products = try JSONDecoder()
    .decode([FailableDecodable].self, from: json)
    .compactMap { $0.base } // .flatMap in Swift 4.0

print(products)

// [
//    GroceryProduct(
//      name: "Banana", points: 200,
//      description: Optional("Une banane cultivée en Équateur.")
//    )
// ]

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], ce qui ne devrait pas poser de problème; cependant, si vous souhaitez l'éviter, vous pourriez toujours créer un autre type wrapper qui décode et déballe chaque élément à partir d'un conteneur sans clé :

struct FailableCodableArray : 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.self).base {

                elements.append(element)
            }
        }

        self.elements = elements
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(elements)
    }
}

Vous pouvez alors décoder comme suit :

let products = try JSONDecoder()
    .decode(FailableCodableArray.self, from: json)
    .elements

print(products)

// [
//    GroceryProduct(
//      name: "Banana", points: 200,
//      description: Optional("Une banane cultivée en Équateur.")
//    )
// ]

1 votes

Et si l'objet de base n'est pas un tableau, mais en contient un ? Comme { "products": [{"name": "banana"...},...] }

2 votes

@ludvigeriksson Vous voulez simplement effectuer le décodage dans cette structure alors, par exemple: gist.github.com/hamishknight/c6d270f7298e4db9e787aecb5b98bca‌​e

2 votes

Le Codable de Swift était facile, jusqu'à présent.. ne pourrait-on pas le rendre un peu plus simple?

54voto

cfergie Points 161

Je créerais un nouveau type Throwable, qui peut envelopper n'importe quel type conforme à Decodable:

enum Throwable: 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].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
        }
    }
}

Je préférerais utiliser un type d'enveloppe enum (par rapport à une Struct) car il peut être utile de suivre les erreurs qui sont lancées ainsi que leurs indices.

Swift 5

Pour Swift 5, envisagez d'utiliser l' enum Result par exemple.

struct Throwable: Decodable {
    let result: Result

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

Pour extraire la valeur décodée, utilisez la méthode get() sur la propriété result:

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

1 votes

J'aime cette réponse parce que je n'ai pas à m'inquiéter d'écrire un init personnalisé.

1 votes

C'est la solution que je cherchais. C'est tellement propre et direct. Merci pour cela!

0 votes

BONNE façon. cela m'aide à bien faire mon travail vraiment. merci.

29voto

Sophy Swicz Points 676

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.

Étant donné que le currentIndex est en lecture seule, une solution consiste à l'incrémenter vous-même en décryptant avec succès un dummy. J'ai utilisé la solution de @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 postée ici est une solution de contournement dans l'un des commentaires. J'aime cette option car je parsse plusieurs modèles de la même manière sur un client réseau, et je voulais que la solution soit spécifique à l'un des objets. Autrement dit, je veux toujours jeter les autres.

J'explique mieux sur mon github https://github.com/phynet/Lossy-array-decode-swift4

import Foundation

    let json = """
    [
        {
            "name": "Banana",
            "points": 200,
            "description": "Une banane cultivée en Équateur."
        },
        {
            "name": "Oranger"
        }
    ]
    """.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) // <-- ASTUCE
                }
            }
            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 votes

Une variation, au lieu d'un if/else j'utilise un do/catch à l'intérieur de la boucle while pour pouvoir enregistrer l'erreur

2 votes

Cette réponse mentionne le suivi des bogues de Swift et a la structure supplémentaire la plus simple (pas de génériques!) donc je pense qu'elle devrait être acceptée.

2 votes

Cela devrait être la réponse acceptée. Toute réponse qui corrompt votre modèle de données est un compromis inacceptable à mon avis.

24voto

vadian Points 29149

Il y a deux options:

  1. Déclarez 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. Écrivez un initialiseur personnalisé pour attribuer des valeurs par défaut dans le cas de 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) ?? ""
        }
    }

5 votes

Au lieu de try? avec decode, il est préférable d'utiliser try avec decodeIfPresent dans la seconde option. Nous devons définir une valeur par défaut seulement s'il n'y a pas de clé, pas en cas d'échec de décodage, comme lorsque la clé existe, mais que le type est incorrect.

0 votes

Hé @vadian, connaissez-vous d'autres questions SO impliquant un initialiseur personnalisé pour assigner des valeurs par défaut en cas de type incorrect? J'ai une clé qui est de type Int mais qui peut parfois être de type String dans le JSON, donc j'ai essayé de faire ce que vous avez dit ci-dessus avec deviceName = try values.decodeIfPresent(Int.self, forKey: .deviceName) ?? 00000 donc si cela échoue, cela mettra simplement 0000 mais cela échoue toujours.

0 votes

Dans ce cas decodeIfPresent est la mauvaise API car la clé existe effectivement. Utilisez un autre bloc do - catch. Décodez String, si une erreur se produit, décodez Int

9voto

Fraser Points 619

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

fileprivate struct DummyCodable: Codable {}

extension UnkeyedDecodingContainer {

    public mutating func decodeArray(_ 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("erreur : \(error)")

                // astuce pour incrémenter currentIndex
                _ = try self.decode(DummyCodable.self)
            }
        }
        return array
    }
}
extension KeyedDecodingContainerProtocol {
    public func decodeArray(_ 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 de l'appeler 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": "Une banane cultivée en Équateur."
    },
    {
        "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)

0 votes

J'ai enveloppé cette solution dans une extension github.com/IdleHandsApps/SafeDecoder

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X