Matrizes de decodificação Swift JSONDecode falharão se a decodificação de elemento único falhar


116

Ao usar os protocolos Swift4 e Codable, tive o seguinte problema - parece que não há como permitir JSONDecoderpular elementos em uma matriz. Por exemplo, tenho o seguinte JSON:

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

E uma estrutura codificável :

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

Ao decodificar este json

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

O resultado productsestá vazio. O que era de se esperar, devido ao fato de que o segundo objeto em JSON não tem "points"chave, enquanto pointsnão é opcional em GroceryProductstruct.

A questão é como posso permitir JSONDecoder"pular" um objeto inválido?


Não podemos ignorar os objetos inválidos, mas você pode atribuir valores padrão se for nulo.
App Vini

1
Por que não pode pointssimplesmente ser declarado opcional?
NRitH

Respostas:


115

Uma opção é usar um tipo de wrapper que tenta decodificar um determinado valor; armazenar nilse malsucedido:

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

Podemos então decodificar uma matriz destes, com o seu GroceryProductpreenchimento no Baseespaço reservado:

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

Em seguida, estamos usando .compactMap { $0.base }para filtrar os nilelementos (aqueles que geraram um erro na decodificação).

Isso criará um array intermediário de [FailableDecodable<GroceryProduct>], o que não deve ser um problema; no entanto, se quiser evitá-lo, você sempre pode criar outro tipo de wrapper que decodifique e desembrulhe cada elemento de um contêiner sem chave:

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

Você então decodificaria 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.")
//    )
// ]

1
E se o objeto base não for um array, mas contiver um? Como {"produtos": [{"name": "banana" ...}, ...]}
ludvigeriksson

2
@ludvigeriksson Você deseja apenas realizar a decodificação dentro dessa estrutura, por exemplo: gist.github.com/hamishknight/c6d270f7298e4db9e787aecb5b98bcae
Hamish

1
Codable do Swift era fácil, até agora .. isso não pode ser um pouco mais simples?
Jonny

@Hamish Não vejo nenhum tratamento de erro para esta linha. O que acontece se um erro for lançado aquivar container = try decoder.unkeyedContainer()
bibscy

@bibscy Está dentro do corpo de init(from:) throws, então o Swift propagará automaticamente o erro de volta para o chamador (neste caso, o decodificador, que o propagará de volta para a JSONDecoder.decode(_:from:)chamada).
Hamish

34

Eu criaria um novo tipo Throwable, que pode envolver qualquer tipo em conformidade com 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 um array de GroceryProduct(ou qualquer outro Collection):

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

onde valueé uma propriedade computada introduzida em uma extensão em Throwable:

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

Eu optaria por usar um enumtipo de wrapper (em vez de a Struct) porque pode ser útil controlar os erros que são lançados, bem como seus índices.

Swift 5

Para Swift 5, considere usar o exemploResult enum

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

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

Para desembrulhar o valor decodificado, use o get()método na resultpropriedade:

let products = throwables.compactMap { try? $0.result.get() }

Gosto dessa resposta porque não preciso me preocupar em escrever nenhum personalizadoinit
Mihai Fratu,

Esta é a solução que procurava. É tão limpo e direto. Obrigado por isso!
naturaln0va

24

O problema é que, ao iterar em um contêiner, o container.currentIndex não é incrementado para que você possa tentar decodificar novamente com um tipo diferente.

Como o currentIndex é somente leitura, uma solução é incrementá-lo você mesmo, decodificando com sucesso um fictício. Peguei a solução @Hamish e escrevi um wrapper com um init personalizado.

Este problema é um bug atual do Swift: https://bugs.swift.org/browse/SR-5953

A solução postada aqui é uma solução alternativa em um dos comentários. Gosto dessa opção porque estou analisando vários modelos da mesma maneira em um cliente de rede e queria que a solução fosse local para um dos objetos. Ou seja, ainda quero que os outros sejam descartados.

Eu explico melhor no meu 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)

1
Uma variação, em vez de if/else, uso um do/catchdentro do whileloop para registrar o erro
Fraser

2
Essa resposta menciona o rastreador de bug do Swift e tem a estrutura adicional mais simples (sem genéricos!), Então acho que deve ser a mais aceita.
Alper

2
Esta deve ser a resposta aceita. Qualquer resposta que corrompa seu modelo de dados é uma troca inaceitável.
Joe Susnick

21

Existem duas opções:

  1. Declara todos os membros da estrutura como opcionais, cujas chaves podem estar ausentes

    struct GroceryProduct: Codable {
        var name: String
        var points : Int?
        var description: String?
    }
  2. Escreva um inicializador personalizado para atribuir valores padrão no 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) ?? ""
        }
    }

5
Em vez de try?com decode, é melhor usar trycom a decodeIfPresentsegunda opção. Precisamos definir o valor padrão apenas se não houver chave, não no caso de qualquer falha de decodificação, como quando a chave existe, mas o tipo está errado.
user28434

Ei @vadian, você conhece alguma outra questão de SO envolvendo o inicializador personalizado para atribuir valores padrão caso o tipo de caso não corresponda? Eu tenho uma chave que é um Int, mas às vezes será uma String no JSON, então tentei fazer o que você disse acima com deviceName = try values.decodeIfPresent(Int.self, forKey: .deviceName) ?? 00000isso, se falhar, ele apenas colocará 0000, mas ainda assim falhará.
Martheli

Neste caso decodeIfPresentestá errado APIporque a chave existe. Use outro do - catchbloco. Decodifique String, se ocorrer um erro, decodifiqueInt
vadian

13

Uma solução possibilitada pelo Swift 5.1, usando o wrapper de propriedade:

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

E então o 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: As coisas do wrapper de propriedade só funcionarão se a resposta puder ser encapsulada em uma estrutura (ou seja: não em uma matriz de nível superior). Nesse caso, você ainda pode envolvê-lo manualmente (com um typealias para melhor legibilidade):

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

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

7

Eu coloquei a solução @sophy-swicz, com algumas modificações, em uma extensão 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)
    }
}

Basta chamá-lo assim

init(from decoder: Decoder) throws {

    let container = try decoder.container(keyedBy: CodingKeys.self)

    self.items = try container.decodeArray(ItemType.self, forKey: . items)
}

Para o exemplo acima:

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)

Eu envolvi esta solução em uma extensão github.com/IdleHandsApps/SafeDecoder
Fraser

3

Infelizmente Swift 4 API não tem initializer failable para init(from: Decoder).

Apenas uma solução que vejo é implementar a decodificação personalizada, fornecendo valor padrão para campos opcionais e possível filtro com os dados necessários:

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

2

Tive um problema semelhante recentemente, mas um pouco diferente.

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

Nesse caso, se um dos elementos em friendnamesArrayfor nulo, todo o objeto será nulo durante a decodificação.

E a maneira certa de lidar com esse caso extremo é declarar o array de strings [String]como um array de strings opcionais [String?]conforme abaixo,

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

2

Eu melhorei o @Hamish's para o caso, que você deseja este comportamento para todos os arrays:

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

1

A resposta de @Hamish é ótima. No entanto, você pode reduzir FailableCodableArraypara:

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

1

Em vez disso, você também pode fazer assim:

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

e, em seguida, ao obtê-lo:

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

0

Eu vim com isso KeyedDecodingContainer.safelyDecodeArrayque fornece uma interface simples:

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

O loop potencialmente infinito while !container.isAtEndé uma preocupação e é abordado usando EmptyDecodable.


0

Uma tentativa muito mais simples: por que você não declara pontos como opcionais ou faz com que a matriz contenha elementos opcionais

let products = [GroceryProduct?]
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.