Swift JSONDecode decoding arrays fails if single element decoding fails
One option is to use a wrapper type that attempts to decode a given value; storing nil
if unsuccessful:
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) }}
We can then decode an array of these, with your GroceryProduct
filling in the Base
placeholder:
import Foundationlet 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.0print(products)// [// GroceryProduct(// name: "Banana", points: 200,// description: Optional("A banana grown in Ecuador.")// )// ]
We're then using .compactMap { $0.base }
to filter out nil
elements (those that threw an error on decoding).
This will create an intermediate array of [FailableDecodable<GroceryProduct>]
, which shouldn't be an issue; however if you wish to avoid it, you could always create another wrapper type that decodes and unwraps each element from an unkeyed container:
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) }}
You would then decode as:
let products = try JSONDecoder() .decode(FailableCodableArray<GroceryProduct>.self, from: json) .elementsprint(products)// [// GroceryProduct(// name: "Banana", points: 200,// description: Optional("A banana grown in Ecuador.")// )// ]
I would create a new type Throwable
, which can wrap any type conforming to 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) } }}
For decoding an array of GroceryProduct
(or any other Collection
):
let decoder = JSONDecoder()let throwables = try decoder.decode([Throwable<GroceryProduct>].self, from: json)let products = throwables.compactMap { $0.value }
where value
is a computed property introduced in an extension on Throwable
:
extension Throwable { var value: T? { switch self { case .failure(_): return nil case .success(let value): return value } }}
I would opt for using a enum
wrapper type (over a Struct
) because it may be useful to keep track of the errors that are thrown as well as their indices.
Swift 5
For Swift 5 Consider using the Result
enum
e.g.
struct Throwable<T: Decodable>: Decodable { let result: Result<T, Error> init(from decoder: Decoder) throws { result = Result(catching: { try T(from: decoder) }) }}
To unwrap the decoded value use the get()
method on the result
property:
let products = throwables.compactMap { try? $0.result.get() }
The problem is that when iterating over a container, the container.currentIndex isn’t incremented so you can try to decode again with a different type.
Because the currentIndex is read only, a solution is to increment it yourself successfully decoding a dummy. I took @Hamish solution, and wrote a wrapper with a custom init.
This problem is a current Swift bug: https://bugs.swift.org/browse/SR-5953
The solution posted here is a workaround in one of the comments.I like this option because I’m parsing a bunch of models the same way on a network client, and I wanted the solution to be local to one of the objects. That is, I still want the others to be discarded.
I explain better in my 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)