Storing Swift structs in UserDefaults
Why Codable works but often isn't the best choice.
05 Jan 2026 · 4 min read
UserDefaults is often the first place we reach for when we want to persist small pieces of data. It's simple, built into the system, and works well for basic types like strings, numbers, and booleans.
But what if we try to store a Swift struct in UserDefaults. Let's look at how to do that.

What UserDefaults can store by default
Out of the box, UserDefaults only supports data types which are also supported by property lists. Those are:
We can also store arrays and dictionaries but only if their elements are one of the types listed above.
Storing a struct in UserDefaults
When we try to store a Swift struct in UserDefaults, the compiler does not complain:
struct Content {let title: String}let content = Content(title: "Some title")let userDefaults = UserDefaults.standarduserDefaults.set(content, forKey: "content")
However, once we run the app, it crashes with the following exception:
Exception: Attempt to insert non-property list object for key content
To store a Swift struct in UserDefaults, we need to convert it to one of the supported types. One common approach is to use Codable and encode the struct into Data:
struct Content: Codable {let title: String}if let contentData = try? JSONEncoder().encode(content) {userDefaults.set(contentData, forKey: "content")}
To read the stored data, we can use a JSON decoder:
if let contentData = userDefaults.data(forKey: "content"),let content = try? JSONDecoder().decode(Content.self, from: contentData) {print(content)}
This pattern is easy to implement. But it comes with an important caveat.
Why the Codable approach is error-prone
The biggest issue with this approach appears when our model changes.
If we later modify Content, for example by renaming a property, changing its type or by adding another property, the JSON decoder will not be able to decode a previously saved object.
Unlike a database-based storage, there's no built-in migration mechanism. Since we don't get any compiler hints or errors when changing the struct, this kind of storage may easily lead to bugs or lost data.
An alternative: storing properties individually
If we want more control and resilience to future changes, we can store individual properties instead of encoding the entire struct:
func writeContent(_ content: Content) {userDefaults.set(content.title, forKey: "contentTitle")userDefaults.set(content.amount, forKey: "contentAmount")}func readContent() -> Content {let title = userDefaults.string(forKey: "contentTitle") ?? ""let amount = (userDefaults.object(forKey:) as? Int) ?? 0return Content(title: title, amount: amount)}
With this approach, we gain flexibility:
- We can add or remove properties without breaking decoding
- We can provide sensible default values
- Renaming properties becomes much safer
The trade-off is a bit more manual work, but in return, we get more predictable behavior as our app evolves.



Newsletter
Like to support my work?
Say hi
Related tags
Articles with related topics
Latest articles and tips



