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

Ao usar os protocolos Swift4 e Codable, tive o seguinte problema – parece que não há como permitir que o JSONDecoder salte elementos em uma matriz. Por exemplo, tenho seguindo o 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? } 

Quando decodificar este json

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

Os products resultantes estão vazios. O que é esperado, devido ao fato de que o segundo object em JSON não possui a chave "points" , enquanto os points não são opcionais na estrutura GroceryProduct .

A pergunta é como posso permitir que o JSONDecoder “pule” o object inválido?

Uma opção é usar um tipo de wrapper que tente decodificar um determinado valor; armazenar nil se malsucedido:

 struct FailableDecodable : 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 desses, com o preenchimento do seu GroceryProduct no placeholder 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.") // ) // ] 

Estamos usando então o .compactMap { $0.base } para filtrar os elementos nil (aqueles que lançaram um erro na decodificação).

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

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

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

Existem duas opções:

  1. Declarar 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 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) ?? "" } } 

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 é incrementar você mesmo decodificando com sucesso um manequim. Eu peguei a solução @Hamish e escrevi um wrapper com um init customizado.

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. Eu gosto desta opção porque estou analisando um monte de modelos da mesma forma em um cliente de rede, e queria que a solução fosse local para um dos objects. 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) 

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

Basta ligar 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) 

Infelizmente, a API do Swift 4 não tem inicializador para o init(from: Decoder) .

Apenas uma solução que vejo é implementar a decodificação personalizada, dando valor padrão para campos opcionais e possível filtro com 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)") } 

Gostaria de criar um novo tipo Throwable , que pode envolver qualquer tipo em conformidade com 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 uma matriz de GroceryProduct (ou qualquer outra Collection ):

 let decoder = JSONDecoder() let throwables = try decoder.decode([Throwable].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 tipo de wrapper enum (sobre um Struct ), porque pode ser útil para acompanhar os erros que são lançados, bem como seus índices. Usando um tipo de wrapper para o elemento da matriz ( GroceryProduct ) em vez do

Eu encontrei o mesmo problema e achei nenhuma das respostas satisfatórias.

Eu tinha a seguinte estrutura:

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

Em um ponto, o backend retornou conteúdo ruim para um elemento. Eu resolvi assim:

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