Search code examples
jsonswiftuijsondecoder

Why does my SwiftUI JSONDecoder give a fatal error?


I'm working on a SwiftUI app which should load questions from a JSON-file and show them to the user one-by-one. I followed a tutorial on how to load JSON-data into the view but it's not working. The JSONDecoder seems to fail, anyone got an idea why?

Here's my code:

CodableBundleExtension.swift

import Foundation

extension Bundle {
    func decode(_ file: String) -> [Question] {
        // Locate the JSON-file.
        guard let url = self.url(forResource: file, withExtension: nil) else {
            fatalError("Failed to locate \(file) in bundle.")
        }
        
        // Create a property for the data
        guard let data = try? Data(contentsOf: url) else {
            fatalError("Failed to load \(file) from bundle.")
        }
        
        // Create a decoder
        let decoder = JSONDecoder()
        
        // Create a property for the decoded data
        guard let loaded = try? decoder.decode([Question].self, from: data) else {
            fatalError("Failed to decode \(file) from bundle.")
        }
        
        // Return the decoded data
        return loaded
    }
}

Questions.swift

import SwiftUI

struct Question: Codable, Identifiable {
    let id: Int
    let question: String
    let gamemode: Int
    let min_players: Int
    let max_platers: Int
    let question_type: Int
    let language: String
    
}

ContentView.swift

import SwiftUI

struct ContentView: View {
    
    var body: some View {
        let questions : [Question] = Bundle.main.decode("questions.json")
        
        ForEach(questions) { item in
            Text(item.question)
        }
        ZStack {
            LinearGradient(
                gradient: Gradient(
                            colors: [.blue, .white]),
                            startPoint: .topLeading,
                            endPoint: .bottomTrailing)
                .edgesIgnoringSafeArea(.all)
            VStack {
                Text("The Questions App")
                    .font(.system(size: 36))
                    .fontWeight(.bold)
                    .foregroundColor(.white)
                    .padding(.top, 40)
                    .padding(.bottom, 40)
                VStack {
                    Image(systemName: "homepod")
                        .renderingMode(.original)
                        .resizable()
                        .frame(width: /*@START_MENU_TOKEN@*/100/*@END_MENU_TOKEN@*/, height: 140, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
                        .aspectRatio(contentMode: .fit)
                        
                    Spacer()
                    
                    Button {
                        print("tapped")
                    } label: {
                        Text("Click Me")
                            .font(.system(size: 32))
                            .fontWeight(.medium)
                            .foregroundColor(.blue)
                            .frame(width: 200, height: 70, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
                            .background(Color.white)
                            .cornerRadius(20)
                            
                    }
                    
                    Spacer()
                }
                
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

questions.json

[
    {
        "id": 1,
        "question": "Testquestion 1",
        "gamemode": 1,
        "min_players": 2,
        "max_players": 4,
        "question_type": 1,
        "language": "NL"
    },
    {
        "id": 2,
        "question": "Testquestion 2",
        "gamemode": 1,
        "min_players": 2,
        "max_players": 99,
        "question_type": 2,
        "language": "NL"
    }
]

And here's the error:

Fatal error: Failed to decode questions.json from bundle.: file Drinking_App_V1/CodableBundleExtension.swift, line 27
2021-04-25 20:19:29.617268+0200 Questions App V1[13218:851770] Fatal error: Failed to decode questions.json from bundle.: file Questions_App_V1/CodableBundleExtension.swift, line 27
(lldb) 

Solution

  • If you use do/catch:

    extension Bundle {
        func decode(_ file: String) -> [Question] {
            // Locate the JSON-file.
            guard let url = self.url(forResource: file, withExtension: nil) else {
                fatalError("Failed to locate \(file) in bundle.")
            }
            
            do {
                let data = try Data(contentsOf: url)
                let decoder = JSONDecoder()
                return try decoder.decode([Question].self, from: data)
            } catch {
                print(error)
                fatalError("Failed to decode \(file) from bundle.")
            }
        }
    }
    

    You'll see this error:

    keyNotFound(CodingKeys(stringValue: "max_platers", intValue: nil), Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key CodingKeys(stringValue: \"max_platers\", intValue: nil) (\"max_platers\").", underlyingError: nil))
    

    This is telling you that there's no value in the JSON for max_platers, which as you can see, is a misspelling of max_players.

    Convert your model to:

    struct Question: Codable, Identifiable {
        let id: Int
        let question: String
        let gamemode: Int
        let min_players: Int
        let max_players: Int //<-- Here
        let question_type: Int
        let language: String
    }