2017-09-21 1 views
11

Lors de l'utilisation de protocoles Swift4 et Codable, j'ai rencontré le problème suivant: il semble qu'il n'y ait aucun moyen de permettre à JSONDecoder d'ignorer des éléments dans un tableau. Par exemple, j'ai JSON suivant:Les matrices de décodage Swift JSONDecode échouent si le décodage d'un seul élément échoue

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

Et codable struct:

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

Lors du décodage de cette 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", tandis que points n'est pas facultatif dans GroceryProduct struct.

Comment puis-je autoriser JSONDecoder à "ignorer" un objet non valide?

+0

Nous ne pouvons pas ignorer les objets non valides mais vous pouvez attribuer des valeurs par défaut si elle est nulle. –

Répondre

23

Une option consiste à utiliser un type d'emballage qui tente de décoder une valeur donnée; le stockage 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) 
    } 
} 

On peut alors décoder un tableau de ceux-ci, avec votre GroceryProduct remplissage dans 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) 
    .flatMap { $0.base } 

print(products) 

// [ 
// GroceryProduct(
//  name: "Banana", points: 200, 
//  description: Optional("A banana grown in Ecuador.") 
// ) 
// ] 

Nous puis en utilisant .flatMap { $0.base } pour filtrer les éléments nil (ceux qui a jeté une erreur sur le décodage).

Cela créera un tableau intermédiaire de [FailableDecodable<GroceryProduct>], ce qui ne devrait pas poser de problème; si vous voulez éviter, vous pouvez toujours créer un autre type d'emballage qui décode et déballe chaque élément d'un conteneur unkeyed:

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

alors vous décodent 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.") 
// ) 
// ] 
+0

merci de la réponse. Je suis surpris que ce soit le seul moyen ... – Remover

+0

Que faire si l'objet de base n'est pas un tableau, mais qu'il en contient un? Comme {"products": [{"name": "banana" ...}, ...]} – ludvigeriksson

+1

@ludvigeriksson Vous voulez juste effectuer le décodage dans cette structure puis, par exemple: https: //gist.github .com/hamishknight/c6d270f7298e4db9e787aecb5b98bcae – Hamish

1

Malheureusement, l'initialisation de l'API Swift 4 n'est pas disponible pour init(from: Decoder).

Une seule solution que je vois est mise en œuvre de décodage personnalisé, donnant une valeur par défaut pour les champs facultatifs et 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)") 
} 
7

Il y a deux options:

  1. Déclarez tous les membres de la structure en option 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 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) ?? "" 
        } 
    } 
    
+1

Au lieu de 'try?' Avec 'decode', il vaut mieux utiliser' try' avec 'decodeIfPresent' dans la deuxième option. Nous avons besoin de définir la valeur par défaut seulement s'il n'y a pas de clé, pas en cas d'échec de décodage, comme quand la clé existe, mais le type est faux. – user28434

+0

@ user28434 Bien sûr, merci. – vadian

+0

hey @vadian connaissez-vous d'autres questions SO impliquant un initialiseur personnalisé pour assigner des valeurs par défaut dans le cas où le type ne correspond pas? J'ai une clé qui est un Int mais parfois sera une chaîne dans le JSON alors j'ai essayé de faire ce que vous avez dit ci-dessus avec 'deviceName = try values.decodeIfPresent (Int.self, forKey: .deviceName) ?? 00000' donc s'il échoue, il mettra juste 0000 mais il échoue toujours. – Martheli

7

Le problème est que lors de l'itération sur un conteneur, l'élément container.currentIndex n'est pas incrémenté, ce qui vous permet de réessayer de décoder avec un type différent.Parce que le currentIndex est en lecture seule, une solution consiste à l'incrémenter vous-même décoder 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 de Swift Current: https://bugs.swift.org/browse/SR-5953

La solution ici est affichée une solution dans l'un des commentaires. J'aime cette option car 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 ma 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) 
+0

Je n'ai pas vu cette réponse, je l'ai implémenté moi-même de cette façon et cela m'a sauvé la peine de trouver moi-même DummyCodable – Fraser

+1

Une variante, au lieu d'un 'if/else' j'utilise un' do/catch' dans le ' while' boucle donc je peux enregistrer l'erreur – Fraser

+0

Cela devrait être la bonne réponse. Je vous remercie –