read parse jsonserialization data arrays json swift swift4 codable

arrays - parse - Las matrices de decodificación Swift JSONDecode fallan si falla la decodificación de un elemento



string to json swift 4 (11)

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

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

Y una estructura codificable :

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 clave de "points" , mientras que los points no son opcionales en la estructura GroceryProduct .

La pregunta es ¿cómo puedo permitir que JSONDecoder "omita" un objeto no válido?


Desafortunadamente, Swift 4 API no tiene un inicializador disponible para init(from: Decoder) .

Solo veo una solución que es la implementación de la decodificación personalizada, que proporciona el valor predeterminado para los campos opcionales y el 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)") }


El problema es que al iterar sobre un contenedor, el container.currentIndex no se incrementa, por lo que puede 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 dummy. 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 montón 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.

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


Encontré el mismo problema y no encontré ninguna de las respuestas satisfactorias.

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 momento, el backend devolvió contenido incorrecto para un elemento. Lo resolví 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) }


Hay dos opciones:

  1. Declarar todos los miembros de la estructura como opcionales 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) ?? "" } }


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

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 anterior:

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)


La respuesta de @ Hamish es genial. Sin embargo, puede reducir FailableCodableArray a:

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


Se me ocurre este KeyedDecodingContainer.safelyDecodeArray que proporciona una interfaz 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 } }

El bucle potencialmente infinito while while !container.isAtEnd es preocupante y se soluciona utilizando EmptyDecodable .


Tuve un problema similar recientemente, pero un poco diferente.

struct Person: Codable { var name: String var age: Int var description: String? var friendnamesArray:[String]? }

En este caso, si uno de los elementos en friendnamesArray es nulo, todo el objeto es nulo durante la decodificación.

Y la forma correcta de manejar este caso límite es declarar la matriz de cadenas [String] como una matriz de cadenas opcionales [String?] Como se muestra a continuación,

struct Person: Codable { var name: String var age: Int var description: String? var friendnamesArray:[String?]? }


Un intento mucho más simple: ¿por qué no declaras puntos como opcionales o haces que la matriz contenga elementos opcionales?

let products = [GroceryProduct?]


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

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

Luego podemos decodificar una variedad de estos, con su producto GroceryProduct completando 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<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.") // ) // ]

Luego estamos usando .compactMap { $0.base } para filtrar elementos nil (aquellos que arrojaron un error en la decodificación).

Esto creará una matriz intermedia de [FailableDecodable<GroceryProduct>] , que no debería ser un problema; sin embargo, si desea evitarlo, siempre puede crear otro tipo de envoltorio que decodifique y desenvuelva cada elemento de un contenedor sin clave:

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

Luego decodificaría como:

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.") // ) // ]


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

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

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

donde 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 envoltorio de enum (sobre un Struct ) porque puede ser útil realizar un seguimiento de los errores que se lanzan, así como sus índices.