Search code examples
jsonswiftparsingnancodable

How to decode NaN value from JSON using Swift?


Null values decoding works well with Codable protocol, but when I have JSON that has NaN, everything crashes, how do I solve this?

I have spent the last couple of days but did not find a solution.

Say, we have the following code:

[{
   "id": 1
   "apples": 193,
   "oranges": NaN,
   "bananas": null,
   "pineapples": 405,
   "watermelons": 13
   "comment": "oranges and bananas have invalid values"
}]

And this struct:

struct Fruits: Codable, Identifiable {
   var id: Int
   var apples: Int?
   var oranges: Int?
   var bananas: Int?
   var pineapples: Int?
   var watermelons: Int?
   var comment: String?
}

How to decode this with no crashes?


Solution

  • since your data is not valid JSON due to NaN (null is ok), you could try this approach where the original data is made into valid json, works very well for me.

    Note: you are also missing a comma after id and watermelons

    struct ContentView: View {
        
        var body: some View {
            Text("testing")
                .onAppear{
                    let json = """
                    [
                        {
                            "id": 1,
                            "apples": 193,
                            "oranges": NaN,
                            "bananas": null,
                            "pineapples": 405,
                            "watermelons": 13,
                            "comment": "oranges and bananas have invalid values"
                        }
                    ]
                    """
                    // simulated api data
                    let data = json.data(using: .utf8)!
                    
                    // convert to string
                    let jsString = String(data: data, encoding: .utf8)!
                    // convert back to data after replacements
                    let newData = jsString.replacingOccurrences(of: "NaN", with: "null").data(using: .utf8)!
                    
                    do {
                        let fruits = try JSONDecoder().decode([Fruits].self, from: newData)
                        print("\n---> fruits: \(fruits)")
                    } catch (let error) {
                        print("\n---> error: \(error)")
                    }
                }
        }
    }
    

    EDIT-1: alternative using JSContext:

    import JavaScriptCore
    
    struct ContentView: View {
        var body: some View {
            Text("using JSContext")
                .onAppear{
                    let json = """
                    [
                        {
                            "id": 1,
                            "apples": 193,
                            "oranges": NaN,
                            "bananas": null,
                            "pineapples": 405,
                            "watermelons": 13,
                            "comment": "oranges and bananas have invalid values"
                        }
                    ]
                    """
                    let fruits = decodeToFruits(json)
                    print("\n---> fruits: \(fruits)")
                }
        }
        
        func decodeToFruits(_ badJson: String) -> [Fruits] {
            if let goodJson = JSContext().evaluateScript("JSON.stringify(\(badJson))"),
               let goodStr = goodJson.toString(),
               let data = goodStr.data(using: .utf8) {
                do {
                    return try JSONDecoder().decode([Fruits].self, from: data)
                } catch (let error) {
                    print("\n---> error: \(error)")
                }
            }
            return []
        }
    }