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?
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.")
// )
// ]
Il y a deux options:
-
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? }
-
É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) ?? "" } }
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)
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)
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 }
Où 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.
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)")
}
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)
}