Las matrices de deencoding Swift JSONDecode fallan si falla la deencoding de un solo elemento

Al usar los protocolos Swift4 y Codable, tuve el siguiente problema: parece que no hay forma de permitir que JSONDecoder omita elementos en una matriz. Por ejemplo, tengo siguiente JSON:

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

Y una estructura Codable :

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

Al decodificar este json

 let decoder = JSONDecoder() let products = try decoder.decode([GroceryProduct].self, from: json) 

Los products resultantes están vacíos. Lo cual es de esperar, debido al hecho de que el segundo objeto en JSON no tiene una clave de "points" , mientras que los points no son opcionales en la estructura de GroceryProduct .

La pregunta es ¿cómo puedo permitir que JSONDecoder “omita” objetos no válidos?

Una opción es usar un tipo de envoltura que intente decodificar un valor dado; almacenando nil si no tiene éxito:

 struct FailableDecodable : Decodable { let base: Base? init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() self.base = try? container.decode(Base.self) } } 

A continuación, podemos decodificar una matriz de estos, con su GroceryProduct llenando el marcador Base posición 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].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.") // ) // ] 

Entonces estamos usando .compactMap { $0.base } para filtrar elementos nil (aquellos que lanzaron un error al decodificar).

Esto creará una matriz intermedia de [FailableDecodable] , que no debería ser un problema; sin embargo, si desea evitarlo, siempre puede crear otro tipo de envoltura que descodifique y desenvuelva cada elemento de un contenedor sin etiqueta:

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

Luego decodificaría como:

 let products = try JSONDecoder() .decode(FailableCodableArray.self, from: json) .elements print(products) // [ // GroceryProduct( // name: "Banana", points: 200, // description: Optional("A banana grown in Ecuador.") // ) // ] 

Hay dos opciones:

  1. Declarar todos los miembros de la estructura como opcional cuyas claves pueden faltar

     struct GroceryProduct: Codable { var name: String var points : Int? var description: String? } 
  2. Escriba un inicializador personalizado para asignar valores predeterminados en el caso 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) ?? "" } } 

El problema es que al iterar sobre un contenedor, container.currentIndex no se incrementa para que pueda intentar decodificar nuevamente con un tipo diferente.

Debido a que currentIndex es de solo lectura, una solución es incrementarlo usted mismo decodificando con éxito un maniquí. Tomé la solución @Hamish y escribí un contenedor con un init personalizado.

Este problema es un error actual de Swift: https://bugs.swift.org/browse/SR-5953

La solución publicada aquí es una solución en uno de los comentarios. Me gusta esta opción porque estoy analizando un conjunto de modelos de la misma manera en un cliente de red, y quería que la solución fuera local para uno de los objetos. Es decir, todavía quiero que los demás sean descartados.

Explico mejor en mi 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) 

He puesto la solución @ sophy-swicz, con algunas modificaciones, en una extensión fácil de usar

 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("error: \(error)") // hack to increment 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) } } 

Solo llámalo así

 init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.items = try container.decodeArray(ItemType.self, forKey: . items) } 

Para el ejemplo de arriba:

 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) 

Lamentablemente, Swift 4 API no tiene inicializador failable para init(from: Decoder) .

Solo una solución que veo es la implementación de deencoding personalizada, dando valor predeterminado para campos opcionales y posible filtro con los datos necesarios:

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

Throwable un nuevo tipo Throwable , que puede envolver cualquier tipo conforme a 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) } } } 

Para decodificar una matriz de GroceryProduct (o cualquier otra Collection ):

 let decoder = JSONDecoder() let throwables = try decoder.decode([Throwable].self, from: json) let products = throwables.compactMap { $0.value } 

donde el value es una propiedad calculada introducida en una extensión en Throwable :

 extension Throwable { var value: T? { switch self { case .failure(_): return nil case .success(let value): return value } } } 

Struct por usar un tipo de envoltura enum (sobre un Struct ) porque puede ser útil hacer un seguimiento de los errores que se lanzan, así como sus índices. Usar un tipo de envoltura para el elemento de la matriz ( GroceryProduct ) en lugar de

Me encontré con el mismo problema y no encontré ninguna de las respuestas satisfactoria.

Tenía la siguiente estructura:

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

En un punto, el servidor devolvió un contenido incorrecto para un elemento. Lo solucioné de esta manera:

  offers = [] for offerData in offersDataArray { if let offer = try? decoder.decode(Offer.self, from: JSONSerialization.data(withJSONObject: offerData, options: .prettyPrinted)) { offers.append(offer) }