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:
-
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? }
-
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.