4

I'm trying to use Swift 4's new JSON Decoding to parse JSON from a remote server. The JSON schema includes enum values, some of which I don't actually need for my purposes and I'd like to ignore. Additionally, I'd also like to be robust enough so that when the JSON schema changes, I will still be able to read as much of the data as I can.

The problem is that when I try to parse an array of anything containing enums, unless every enum value matches my enum's literals exactly, the decoder throws an exception rather than skipping over the data it can't parse.

Here's a simple example:

enum Size: String, Codable {
    case large = "large"
    case small = "small"
}

enum Label: String, Codable {
    case kitchen = "kitchen"
    case bedroom = "bedroom"
    case livingRoom = "living room"
}

struct Box: Codable {
    var contents: String
    var size: Size
    var labels: [Label]
}

When I parse data that completely conforms to my Size enum, I get the expected results:

let goodJson = """
[
  {
    "contents": "pillows",
    "size": "large",
    "labels": [
        "bedroom"
    ]
  },
  {
    "contents": "books",
    "size": "small",
    "labels": [
        "bedroom",
        "living room"
    ]
  }
]
""".data(using: .utf8)!

let goodBoxes = try? JSONDecoder().decode([Box?].self, from: goodJson)
// returns [{{contents "pillows", large, [bedroom]}},{{contents "books", small, [bedroom, livingRoom]}}]

However, if there are contents that don't conform to the enum, the decoder throws an exception and I get nothing back.

let badJson = """
[
  {
    "contents": "pillows",
    "size": "large",
    "labels": [
        "bedroom"
    ]
  },
  {
    "contents": "books",
    "size": "small",
    "labels": [
        "bedroom",
        "living room",
        "office"
    ]
  },
  {
    "contents": "toys",
    "size": "medium",
    "labels": [
        "bedroom"
    ]
  }
]
""".data(using: .utf8)!

let badBoxes = try? JSONDecoder().decode([Box?].self, from: badJson)    // returns nil

Ideally, in this case, I would like to get back the 2 items where size conforms to "small" or "large" and the second item wound have the 2 valid labels, "bedroom" and "living room".

If I implement my own init(from: decoder) for Box, I could decode labels myself and throw out any which don't conform to my enum. However, I can't figure out how to decode a [Box] type to ignore invalid boxes without implementing my own Decoder and parsing the JSON myself, which defeats the purpose of using Codable.

Any ideas?

Mel
  • 959
  • 5
  • 19
  • I have a similar case, where I just want to ignore invalid array elements and don't want the parser to fail on the whole array. Is there really no way to skip invalid elements, instead of completely "throw" the array away and fail the whole parsing? :-/ – d4Rk Sep 19 '17 at 10:19
  • Compare https://stackoverflow.com/q/46344963/2976878 – Hamish Oct 07 '17 at 11:14

2 Answers2

0

I will admit that this isn't the prettiest solution but it's what I came up with and I thought I would share. I created an extension on Array that allows for this. The biggest con is that you have to decode and then encode the JSON data again.

extension Array where Element: Codable {
    public static func decode(_ json: Data) throws -> [Element] {
        let jsonDecoder = JSONDecoder()
        var decodedElements: [Element] = []
        if let jsonObject = (try? JSONSerialization.jsonObject(with: json, options: [])) as? Array<Any> {
            for json in jsonObject {
               if let data = try? JSONSerialization.data(withJSONObject: json, options: []), let element = (try? jsonDecoder.decode(Element.self, from: data)) {
                    decodedElements.append(element)
                }
            }
        }
        return decodedElements
    }
}

You can use this extension with anything that conforms to Codable and should solve your problem.

[Box].decode(json)

Unfortunately I don't see how this will fix the issue with the labels being incorrect and you will have to do as you say and override init(from: Decoder) to make sure that your labels are valid.

0

A bit of a pain but you can write the decoding yourself.

import Foundation

enum Size: String, Codable {
    case large = "large"
    case small = "small"
}

enum Label: String, Codable {
    case kitchen = "kitchen"
    case bedroom = "bedroom"
    case livingRoom = "living room"
}

struct Box: Codable {
    var contents: String = ""
    var size: Size = .small
    var labels: [Label] = []

    init(from decoder: Decoder) throws {
        guard let container = try? decoder.container(keyedBy: CodingKeys.self) else {
            return
        }

        contents = try container.decode(String.self, forKey: .contents)
        let rawSize = try container.decode(Size.RawValue.self, forKey: .size)
        size = Size(rawValue: rawSize) ?? .small

        var labelsContainer = try container.nestedUnkeyedContainer(forKey: .labels)
        while !labelsContainer.isAtEnd {
            let rawLabel = try labelsContainer.decode(Label.RawValue.self)
            if let label = Label(rawValue: rawLabel) {
                labels.append(label)
            }
        }
    }
}

extension Box: CustomStringConvertible {
    var description: String {
        let encoder = JSONEncoder()
        encoder.outputFormatting = .prettyPrinted
        do {
            let data = try encoder.encode(self)
            if let jsonString = String(data: data, encoding: .utf8) {
                return jsonString
            }
        } catch {
            return ""
        }
        return ""
    }
}

let badJson = """
[
  {
    "contents": "pillows",
    "size": "large",
    "labels": [
        "bedroom"
    ]
  },
  {
    "contents": "books",
    "size": "small",
    "labels": [
        "bedroom",
        "living room",
        "office"
    ]
  },
  {
    "contents": "toys",
    "size": "medium",
    "labels": [
        "bedroom"
    ]
  }
]
""".data(using: .utf8)!

do {
    let badBoxes = try JSONDecoder().decode([Box].self, from: badJson)
    print(badBoxes)
} catch {
    print(error)
}

Output:

[{
  "labels" : [
    "bedroom"
  ],
  "size" : "large",
  "contents" : "pillows"
}, {
  "labels" : [
    "bedroom",
    "living room"
  ],
  "size" : "small",
  "contents" : "books"
}, {
  "labels" : [
    "bedroom"
  ],
  "size" : "small",
  "contents" : "toys"
}]
xissburg
  • 618
  • 5
  • 14