Las matrices de decodificación Swift JSONDecode fallan si falla la decodificación de un solo elemento

116

Mientras usaba los protocolos Swift4 y Codable, tuve el siguiente problema: parece que no hay forma de permitir JSONDecoderomitir 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)

El resultado productsestá vacío. Lo cual es de esperar, debido al hecho de que el segundo objeto en JSON no tiene "points"clave, mientras pointsque no es opcional enGroceryProduct struct.

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

Khriapin Dmitriy
fuente
No podemos omitir los objetos no válidos, pero puede asignar valores predeterminados si es nulo.
Aplicación Vini
1
¿Por qué no se pointspuede declarar simplemente opcional?
NRitH

Respuestas:

115

Una opción es utilizar un tipo de contenedor que intente decodificar un valor dado; almacenar nilsi 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 serie de estos, con su GroceryProductrelleno en el Basemarcador de posición:

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 filtrarnil elementos (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 contenedor 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.")
//    )
// ]
Hamish
fuente
1
¿Qué pasa si el objeto base no es una matriz, pero contiene una? Como {"products": [{"name": "banana" ...}, ...]}
ludvigeriksson
2
@ludvigeriksson Solo desea realizar la decodificación dentro de esa estructura, por ejemplo: gist.github.com/hamishknight/c6d270f7298e4db9e787aecb5b98bcae
Hamish
1
Swift's Codable era fácil, hasta ahora ... ¿no se puede simplificar un poco?
Jonny
@Hamish No veo ningún manejo de errores para esta línea. ¿Qué ocurre si se produce un error aquívar container = try decoder.unkeyedContainer()
bibscy
@bibscy Está dentro del cuerpo de init(from:) throws, por lo que Swift propagará automáticamente el error a la persona que llama (en este caso, el decodificador, que lo propagará de regreso a la JSONDecoder.decode(_:from:)llamada).
Hamish
33

Crearía 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 matriz 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 valuees 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
        }
    }
}

Optaría por usar un enumtipo de envoltorio (sobre unStruct ) porque puede ser útil para realizar un seguimiento de los errores que se producen, así como de sus índices.

Rápido 5

Para Swift 5, considere usar el ejemploResult enum

struct Throwable<T: Decodable>: Decodable {
    let result: Result<T, Error>

    init(from decoder: Decoder) throws {
        result = Result(catching: { try T(from: decoder) })
    }
}

Para desenvolver el valor decodificado, use el get()método de la resultpropiedad:

let products = throwables.compactMap { try? $0.result.get() }
cfergie
fuente
Me gusta esta respuesta porque no tengo que preocuparme por escribir ninguna costumbreinit
Mihai Fratu
Esta es la solución que estaba buscando. Es tan limpio y sencillo. ¡Gracias por esto!
naturaln0va
24

El problema es que cuando se itera sobre un contenedor, el contenedor.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 exitosamente 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 alternativa 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 se descarten los demás.

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)
Sophy Swicz
fuente
1
Una variación, en lugar de un if/elseuso do/catchdentro del whileciclo para poder registrar el error
Fraser
2
Esta respuesta menciona el rastreador de errores Swift y tiene la estructura adicional más simple (¡sin genéricos!) Así que creo que debería ser la aceptada.
Alper
2
Esta debería ser la respuesta aceptada. Cualquier respuesta que corrompa su modelo de datos es una compensación inaceptable en mi opinión.
Joe Susnick
21

Hay dos opciones:

  1. Declare 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 nilcaso.

    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) ?? ""
        }
    }
vadian
fuente
5
En lugar de try?con decode, es mejor usar trycon decodeIfPresenten la segunda opción. Necesitamos establecer el valor predeterminado solo si no hay una clave, no en caso de falla de decodificación, como cuando existe una clave, pero el tipo es incorrecto.
user28434
Hola @vadian, ¿conoces alguna otra pregunta SO relacionada con el inicializador personalizado para asignar valores predeterminados en caso de que el tipo no coincida? Tengo una clave que es un Int, pero a veces será una cadena en el JSON, así que intenté hacer lo que dijiste anteriormente, deviceName = try values.decodeIfPresent(Int.self, forKey: .deviceName) ?? 00000así que si falla, solo pondrá 0000 pero aún falla.
Martheli
En este caso decodeIfPresentestá mal APIporque la clave existe. Usa otro do - catchbloque. Decodificar String, si ocurre un error, decodificarInt
vadian
13

Una solución hecha posible por Swift 5.1, utilizando el contenedor de propiedades:

@propertyWrapper
struct IgnoreFailure<Value: Decodable>: Decodable {
    var wrappedValue: [Value] = []

    private struct _None: Decodable {}

    init(from decoder: Decoder) throws {
        var container = try decoder.unkeyedContainer()
        while !container.isAtEnd {
            if let decoded = try? container.decode(Value.self) {
                wrappedValue.append(decoded)
            }
            else {
                // item is silently ignored.
                try? container.decode(_None.self)
            }
        }
    }
}

Y luego el uso:

let json = """
{
    "products": [
        {
            "name": "Banana",
            "points": 200,
            "description": "A banana grown in Ecuador."
        },
        {
            "name": "Orange"
        }
    ]
}
""".data(using: .utf8)!

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

struct ProductResponse: Decodable {
    @IgnoreFailure
    var products: [GroceryProduct]
}


let response = try! JSONDecoder().decode(ProductResponse.self, from: json)
print(response.products) // Only contains banana.

Nota: Las cosas del contenedor de propiedades solo funcionarán si la respuesta se puede empaquetar en una estructura (es decir, no en una matriz de nivel superior). En ese caso, aún puede ajustarlo manualmente (con un tipo alias para una mejor legibilidad):

typealias ArrayIgnoringFailure<Value: Decodable> = IgnoreFailure<Value>

let response = try! JSONDecoder().decode(ArrayIgnoringFailure<GroceryProduct>.self, from: json)
print(response.wrappedValue) // Only contains banana.
rraphael
fuente
7

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)
Fraser
fuente
Envolví esta solución en una extensión github.com/IdleHandsApps/SafeDecoder
Fraser
3

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

Solo una solución que veo es implementar la decodificación personalizada, dando un valor predeterminado para los campos opcionales y un 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)")
}
dimpiax
fuente
2

Recientemente tuve un problema similar, 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 friendnamesArrayes nulo, todo el objeto es nulo mientras se decodifica.

Y la forma correcta de manejar este caso de borde 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?]?
}
cnu
fuente
2

Mejoré en @ Hamish para el caso, que desea este comportamiento para todas las matrices:

private struct OptionalContainer<Base: Codable>: Codable {
    let base: Base?
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        base = try? container.decode(Base.self)
    }
}

private struct OptionalArray<Base: Codable>: Codable {
    let result: [Base]
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let tmp = try container.decode([OptionalContainer<Base>].self)
        result = tmp.compactMap { $0.base }
    }
}

extension Array where Element: Codable {
    init(from decoder: Decoder) throws {
        let optionalArray = try OptionalArray<Element>(from: decoder)
        self = optionalArray.result
    }
}
Sören Schmaljohann
fuente
1

La respuesta de @ Hamish es genial. Sin embargo, puede reducirlo FailableCodableArraya:

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)
    }
}
Robert Crabtree
fuente
1

En su lugar, también puede hacer esto:

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

y luego adentro mientras lo obtengo:

'let groceryList = try JSONDecoder().decode(Array<GroceryProduct>.self, from: responseData)'
Kalpesh Thakare
fuente
0

Se me ocurre esto KeyedDecodingContainer.safelyDecodeArrayque 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 !container.isAtEndes una preocupación y se aborda mediante el uso de EmptyDecodable.

Haoxin Li
fuente
0

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

let products = [GroceryProduct?]
BobbelKL
fuente