Swift JSONDecode decoding arrays fails if single element decoding fails

Khriapin Dmitriy picture Khriapin Dmitriy · Sep 21, 2017 · Viewed 62.6k times · Source

While using Swift4 and Codable protocols I got the following problem - it looks like there is no way to allow JSONDecoder to skip elements in an array. For example, I have the following JSON:

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

And a Codable struct:

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

When decoding this json

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

Resulting products is empty. Which is to be expected, due to the fact that the second object in JSON has no "points" key, while points is not optional in GroceryProduct struct.

Question is how can I allow JSONDecoder to "skip" invalid object?

Answer

Hamish picture Hamish · Sep 22, 2017

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

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)
    .elements

print(products)

// [
//    GroceryProduct(
//      name: "Banana", points: 200,
//      description: Optional("A banana grown in Ecuador.")
//    )
// ]