Home > Net >  Swift JSON Decoder
Swift JSON Decoder

Time:04-18

Happy Easter!

I'm building an application to take in 3 different JSON files. Each of which are comprised of the same numbers as options to win a game(darts outs).

Example:

{
    "checkoutOptions1": [
        {
            "170": {
                "out": "T20 T20 DB"
            },
            
            "167": {
                "out": "T20 T19 DB"
            },
            
            "164": {
                "out": "T20 T18 DB"
            },
            
            "161": {
                "out": "T20 T17 DB"
            },
            
            "160": {
                "out": "T20 T20 D20"
            }
        }
    ]
}

The other 2 files are the same numbers but the out string given is completely different.

I have a struct called "Out" which allows for the "out" to be taken in from the json file in question.

struct Out: Codable {
    let out: String
}

My Bundle decoder is defined as follows:

extension Bundle {
    func decode(_ file: String) -> [String: Out] {
        guard let url = self.url(forResource: file, withExtension: nil) else {
            fatalError("Failed to locate \(file) in bundle.")
        }
        guard let data = try? Data(contentsOf: url) else {
            fatalError("Failed to load \(file) from bundle.")
        }
        let decoder = JSONDecoder()
        
        guard let loaded = try? decoder.decode([String: Out].self, from: data ) else {
            fatalError("Failed to decode \(file) from bundle.")
        }
        
        return loaded
    }
}

It's not working. This is the first time I'm dealing with JSON decoder. Looking for some insight to see if I have the right idea or not. TIA

CodePudding user response:

It's always helpful to wrap the decode into try catch and print the error. The error would tell you where the problem is. If you did it like this:

do {
  let loaded = try decoder.decode([String: Out].self, from: data )
  return loaded
} catch {
  print(error)
  fatalError("Failed to decode \(file) from bundle.")
}

then it would print the following:

typeMismatch(Swift.Dictionary<Swift.String, Any>, Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "checkoutOptions1", intValue: nil)], debugDescription: "Expected to decode Dictionary<String, Any> but found an array instead.", underlyingError: nil))

Indeed, if you look closely then the structure of your JSON is actually like this: [String: [[String: Out]]]. The value of the checkoutOptions1 is an array containing one dictionary of type [String: Out].

So if you change your code to this:

let loaded = try decoder.decode([String: [[String: Out]]].self, from: data )

it will succeed, though the result won't be that easy to deal with since your Out structures would be buried few levels down in the dictionary.

One way to improve this is to define the following model:

struct Options: Codable {
    let checkoutOptions1: [[String: Out]]
}

and then change your decode line to this:

let loaded = try decoder.decode(Options.self, from: data)

The loaded.checkoutOptions1[0] will contain the dictionary of type [String: Out]

CodePudding user response:

your model (Out) does not match the json data that you show. Try something like this:

struct Checkout: Codable {
    var checkoutOptions1: [[String : Out]]
}

struct Out: Codable {
    var out: String
}

and

 guard let loaded = try? decoder.decode(Checkout.self, from: data) else {
        fatalError("Failed to decode \(file) from bundle.")
    }

EDIT-1: here is a simple example of how to use the models

struct Checkout: Codable {
    var checkoutOptions1: [[String : Out]]
}

struct Out: Codable {
    var out: String
}

struct ContentView: View {
    
    var body: some View {
        Text("testing")
            .onAppear {
                let json = """
{
    "checkoutOptions1": [
        {
            "170": {
                "out": "T20 T20 DB"
            },
            
            "167": {
                "out": "T20 T19 DB"
            },
            
            "164": {
                "out": "T20 T18 DB"
            },
            
            "161": {
                "out": "T20 T17 DB"
            },
            
            "160": {
                "out": "T20 T20 D20"
            }
        }
    ]
}

"""
                
                let data = json.data(using: .utf8)!
                let loaded = try? JSONDecoder().decode(Checkout.self, from: data)
                print("\n----> loaded: \(loaded?.checkoutOptions1) \n")
                
            }
    }
}

Alternatively:

extension Bundle {
    
    func decode<T: Codable>(_ file: String) -> T {
        guard let url = Bundle.main.url(forResource: file, withExtension: nil) else {
            fatalError("Could not find \(file) in the project")
        }
        
        guard let data = try? Data(contentsOf: url) else {
            fatalError("Could not load \(file) in the project")
        }
        
        do {
            let loadedData = try JSONDecoder().decode(T.self, from: data)
            return loadedData
        } catch {
            print(error)
            fatalError("Could not decode \(file) in the project")
        }
    }
}

struct ContentView: View {
    var body: some View {
        Text("testing")
            .onAppear {
                let result: Checkout = Bundle().decode("Dart.json")
                print("\n----> result: \(result) ")
                print("\n----> checkoutOptions1: \(result.checkoutOptions1) ")
                print("\n----> first: \(result.checkoutOptions1.first) ")
                print("\n----> 170: \(result.checkoutOptions1.first?["170"]) ")
                print("\n----> 170 out: \(result.checkoutOptions1.first?["170"]?.out) ")
            }
    }
}
  • Related