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