.json file to your Xcode target, locate it using Bundle.main.url(forResource:withExtension:), load it into a Data instance, and decode it into a Decodable struct using JSONDecoder().decode(YourType.self, from: data). If the file was saved at runtime, read it from FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) instead.Why this happens
Reading local JSON in Swift often leads to unexpected nil values or runtime crashes for two primary reasons: locating the file at the wrong filesystem location, and mismatching the serialized JSON structure with Swift’s decoding models.
First, iOS applications run in a sandboxed environment. Files packaged alongside your source code reside inside the read-only application bundle (Bundle.main). Developers often confuse the app bundle with the app’s writable sandbox containers, such as the Documents directory (FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)). Attempting to find a bundled file using NSSearchPathForDirectoriesInDomains or looking in the documents folder will return an invalid path because the file was never copied there during installation.
Second, legacy parsing techniques relying on JSONSerialization and loosely typed NSDictionary or NSArray objects fail silently when force-casting types. Modern Swift uses the Decodable protocol. If the data types in the JSON payload do not strictly align with the Swift struct—such as treating numbers as strings, missing optional fields, or encountering inconsistent key naming—JSONDecoder throws an explicit error that halts parsing.
How to fix it
1. Model the data and decode from the app bundle
The most common scenario is bundling a static .json configuration or mock data file inside your Xcode project. Ensure the JSON file is added to your target’s Copy Bundle Resources build phase, then decode it using type-safe Swift structs.
Given a JSON file named person_data.json:
{
"person": [
{
"name": "Bob",
"age": 16,
"employed": false
},
{
"name": "Vinny",
"age": 56,
"employed": true
}
]
}
Define your models conforming to Decodable (or Codable), then read the file from Bundle.main:
import Foundation
struct PersonResponse: Decodable {
let person: [Person]
}
struct Person: Decodable, Identifiable {
var id: String { name }
let name: String
let age: Int
let employed: Bool
}
func loadBundledJSON<T: Decodable>(filename: String) -> T? {
guard let url = Bundle.main.url(forResource: filename, withExtension: "json") else {
print("File not found: \(filename).json in main bundle.")
return nil
}
do {
let data = try Data(contentsOf: url)
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
} catch {
print("Failed to load or decode \(filename).json: \(error)")
return nil
}
}
You can then call this function directly in your SwiftUI view or view model:
import SwiftUI
struct ContentView: View {
@State private var people: [Person] = []
var body: some View {
NavigationStack {
List(people) {
Text("\($0.name), Age \($0.age)")
}
.navigationTitle("People")
.task {
if let response: PersonResponse = loadBundledJSON(filename: "person_data") {
people = response.person
}
}
}
}
}
2. Read JSON generated at runtime from the Documents directory
If your application writes JSON to disk while running, that file will live in the app’s sandboxed Documents directory, not the read-only main bundle. Access the documents folder using FileManager:
import Foundation
func loadFromDocumentsDirectory<T: Decodable>(filename: String) -> T? {
let fileManager = FileManager.default
guard let documentsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else {
print("Could not resolve Documents directory.")
return nil
}
let fileURL = documentsURL.appendingPathComponent(filename).appendingPathExtension("json")
guard fileManager.fileExists(atPath: fileURL.path) else {
print("File does not exist at path: \(fileURL.path)")
return nil
}
do {
let data = try Data(contentsOf: fileURL)
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
} catch {
print("Error decoding file from Documents: \(error)")
return nil
}
}
3. Diagnose explicit decoding errors
When JSONDecoder fails, it throws specific DecodingError cases detailing exactly which key or index broke parsing. Replace generic error printing with structured pattern matching:
func decodeDetailed<T: Decodable>(_ type: T.Type, from data: Data) -> T? {
let decoder = JSONDecoder()
do {
return try decoder.decode(T.self, from: data)
} catch DecodingError.keyNotFound(let key, let context) {
print("Missing key: '\(key.stringValue)', context: \(context.debugDescription)")
} catch DecodingError.typeMismatch(let type, let context) {
print("Type mismatch for type: \(type), context: \(context.debugDescription)")
} catch DecodingError.valueNotFound(let type, let context) {
print("Expected non-null value for type: \(type), context: \(context.debugDescription)")
} catch DecodingError.dataCorrupted(let context) {
print("Corrupted data: \(context.debugDescription)")
} catch {
print("Unexpected error: \(error)")
}
return nil
}
4. Handle mismatching keys and snake_case formatting
If your local JSON uses snake_case keys (like user_name or is_employed), configure your JSONDecoder rather than writing manual decoding initializers:
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
For keys that do not follow regular snake_case patterns, declare explicit CodingKeys in your struct:
struct Person: Decodable {
let name: String
let age: Int
let isEmployed: Bool
enum CodingKeys: String, CodingKey {
case name
case age
case isEmployed = "employed"
}
}
How to prevent it
- Check Target Membership in Xcode: Select the
.jsonfile in the Project Navigator, open the File Inspector (Option-Command-1), and verify that your target is checked under Target Membership. - Verify bundle inclusion: Open your project settings, choose your app target, click Build Phases, expand Copy Bundle Resources, and verify your JSON file appears in the list.
- Match strict JSON types: Ensure your struct property types align with JSON primitives. A string containing numbers (
"16") will throw atypeMismatcherror if decoded directly into anInt. - Use optional properties for non-guaranteed keys: If a JSON key can be omitted or null, declare the corresponding Swift struct property as optional (
String?) to preventkeyNotFoundandvalueNotFoundfailures.
FAQ
Can I write back to a JSON file bundled inside the app?
No. The application bundle (Bundle.main) is signed and strictly read-only on iOS. If your app needs to update or overwrite JSON content at runtime, save the modified data to the Documents or Application Support directory via FileManager.
Why does Bundle.main.url(forResource:withExtension:) return nil?
This occurs when the file is not part of the current build target. Ensure the file has its Target Membership checkbox selected in the Xcode File Inspector and is listed under the target’s Copy Bundle Resources build phase.
How can I decode JSON containing string numbers into Swift numeric types?
If a legacy JSON payload provides numbers inside strings (e.g., "age": "16"), decode the property as a String and expose a computed property to parse it into an Int, or write a custom init(from decoder: Decoder) to handle the conversion safely.
Parts of this guide are adapted from content licensed under CC BY-SA 4.0; this article is shared under the same license.
