Codable converts typed values to an external representation; FileManager and Data APIs place that representation on disk. Combining them creates a useful small persistence boundary for settings or modest documents. It also introduces filesystem errors, ownership questions and compatibility obligations that an in-memory model does not have.
Review the Codable lesson. This reference uses a uniquely named temporary file for a safe runnable demonstration. A real app should choose the appropriate sandbox directory and backup policy, and should not treat a temporary-directory example as a permanent user-data location.
Model storage as several fallible steps
Encoding can fail according to the model and encoder. Writing can fail because of permissions, disk capacity or an unavailable destination. Reading can fail because a file is missing or inaccessible, and decoding can fail when stored data is corrupt or no longer matches the schema. Preserve those distinctions when the user needs a meaningful recovery choice.
Use URL-based paths and appendingPathComponent to build destinations under a controlled directory. Do not concatenate untrusted path text and assume it stays within an authorized location. If an interface accepts a filename, constrain it to the supported policy or map a domain identifier to an application-controlled filename.
Directories have different purposes. Documents may contain user-managed persistent content, application support can hold application-owned data, caches can be recreated, and temporary files can disappear. Check the current platform's sandbox and backup conventions before choosing a location. Credentials belong in an appropriate secure storage mechanism, not a casually encoded JSON settings file.
Atomic replacement is a narrow guarantee
The atomic writing option uses an auxiliary file and replacement strategy so the destination is not ordinarily exposed as a partially written new document. This is useful when replacing a small whole-file snapshot. It does not make several files one transaction, prevent another process from overwriting your changes, or provide every possible durability guarantee under hardware failure.
Two writers can both read an old version, compute different changes and replace the file in sequence, losing one update. Coordinate ownership through an actor or another appropriate mechanism inside the app, and use stronger persistence facilities when the data requires cross-process coordination or transactional records.
Whole-file JSON storage also scales with document size. Reading all bytes and decoding a large model can use substantial memory and block the calling thread. Keep the workload modest, move expensive work under a deliberate concurrency policy, and consider a database when incremental updates, queries or large collections become central requirements.
Schema evolution and recovery are part of persistence
Stored files can outlive one app release. Renaming a Codable key, changing a required field or altering a type can make old documents fail to decode. Use explicit CodingKeys and defaults or migrations according to a documented format version. A current compiler accepting the model does not prove compatibility with yesterday's file.
Missing settings may reasonably use defaults on first launch. Corrupt user documents should not be overwritten automatically just because decoding failed. Preserve the original where appropriate and provide a repair, export or reset choice. Silent fallback can turn a recoverable problem into permanent data loss.
Test round trips, old fixtures, missing files and invalid data. Avoid comparing raw JSON key order unless the storage contract explicitly requires it; decode and compare meaningful values instead. Keep personal data out of error logs and do not expose machine-specific absolute paths when a useful category is sufficient.
Do not use a file modification timestamp as a complete conflict-resolution protocol. Clock granularity and competing changes can make that observation insufficient to prove which content a writer started from. A deliberate version field or stronger storage mechanism can express the comparison more reliably. The store's public contract should say whether it replaces a snapshot unconditionally or rejects changes based on an expected version.
Official references
Read FileManager and atomic writing. The official Data source provides implementation context. Review destination ownership, schema compatibility, recovery policy and writer coordination separately from whether one round-trip example succeeds.
Swift FileManager and Codable File Storage Examples
Each example shows the code, the output it produces and what each part does.
Write and read an isolated temporary document
Expected output assumes the temporary directory is writable and the operation succeeds. UUID makes this demonstration independent of other files with the same human-readable name. The cleanup removes only that specific generated file. The generic catch is a concise console boundary; a product should preserve a useful error category instead of reducing every failure to one unexplained message.
import Foundation
struct Settings: Codable {
let theme: String
let pageSize: Int
}
func demo() throws {
let file = FileManager.default.temporaryDirectory
.appendingPathComponent("settings-\(UUID().uuidString).json")
defer { try? FileManager.default.removeItem(at: file) }
let data = try JSONEncoder().encode(Settings(theme: "dark", pageSize: 20))
try data.write(to: file, options: .atomic)
let loaded = try JSONDecoder().decode(Settings.self, from: Data(contentsOf: file))
print("\(loaded.theme):\(loaded.pageSize)")
}
do { try demo() } catch { print("storage failed") }dark:20Swift FileManager and Codable File Storage in Real Projects
Worked exercise: Create a typed settings store
Encapsulate one application-chosen file URL. Save a typed value using atomic replacement and load it through the same decoder. The store throws rather than silently substituting defaults for every error, leaving recovery policy to its caller.
import Foundation
struct Preferences: Codable { let compact: Bool; let pageSize: Int }
struct PreferencesStore {
let file: URL
func save(_ value: Preferences) throws {
let data = try JSONEncoder().encode(value)
try data.write(to: file, options: .atomic)
}
func load() throws -> Preferences {
try JSONDecoder().decode(Preferences.self, from: Data(contentsOf: file))
}
}
func exercise() throws {
let file = FileManager.default.temporaryDirectory
.appendingPathComponent("preferences-\(UUID().uuidString).json")
defer { try? FileManager.default.removeItem(at: file) }
let store = PreferencesStore(file: file)
try store.save(Preferences(compact: true, pageSize: 12))
let loaded = try store.load()
print(loaded.compact)
print(loaded.pageSize)
}
do { try exercise() } catch { print("preferences failed") }true
12Validate pageSize at the domain boundary before saving if only a particular range is allowed. This store is a single-caller example and does not claim concurrent writer coordination. Tests can inject their own temporary URL, making storage ownership and cleanup precise without reaching into a user's real preferences.
Swift FileManager and Codable File Storage FAQ
Does atomic mean a database transaction?
No. It describes the replacement behavior of this one write operation. It does not coordinate several documents, merge competing writers or reverse a separate external effect. Use an actor or another ownership strategy for in-process access and a database or coordinated storage mechanism when the domain requires broader transactional guarantees. Read the actual API contract before treating a useful whole-file replacement option as a universal consistency or durability promise.
Should decoding failure always reset settings?
Not automatically. A missing first-launch file, an unreadable file and a corrupt document have different meanings. Defaults may be appropriate for absence, while corrupt user content may need preservation and a recovery choice. Blindly writing defaults after every decode error can destroy information that could have been repaired. Keep the loading error visible to the layer responsible for policy, and test old-version fixtures before deciding a file is invalid.
Can I store access tokens in the JSON model?
Use the platform's appropriate secure credential storage instead of a plain document chosen for convenience. Codable controls representation, not encryption or access authorization. File permissions and sandboxing help establish boundaries but do not turn every JSON file into a secret store. Keep credentials out of example payloads and logs, and give the settings model only the nonsecret preferences it actually needs. Security-sensitive values deserve a separate deliberate storage contract.
When should whole-file storage become a database?
Consider a database when data grows large, queries and partial updates matter, several records need coordinated changes, or competing writers require stronger consistency. A small JSON snapshot can remain clear for modest settings. Measure actual size and update patterns instead of migrating solely because databases sound more professional. The important point is matching the persistence contract to access patterns, recovery needs and transaction boundaries rather than extending a simple file store indefinitely.
Swift FileManager and Codable File Storage Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Two callers can update the same file from the same old snapshot. What is still required?
Swift FileManager and Codable File Storage Quiz
2 questions to check what you learned.
What does an atomic whole-file write primarily help avoid?
- Every possible concurrent writer conflict across all processes.
- Exposing the destination as a partially written new document during replacement.
- All JSON schema evolution errors.
Why keep missing and corrupt files distinguishable?
- Corrupt files always prove that no user data existed.
- Decoding errors cannot ever occur.
- First-launch defaults and recovery of damaged data can need different policies.