Xcode warning: Immutable property will not be decoded because it is declared with an initial value which cannot be overwritten
Noah's explanation is correct. It’s a common source of bugs and it's not immediately obvious what’s happening due to the “magical” behaviour of Codable synthesis, which is why I added this warning to the compiler, since it brings your attention to the fact that the property won't be decoded and makes you explicitly call it out if that's the expected behaviour.
As the fix-it explains, you have a couple of options if you want to silence this warning - which one you choose depends on the exact behaviour you want:
- Pass the initial value via an
init
:
struct ExampleItem: Decodable {
let number: Int
init(number: Int = 42) {
self.number = number
}
}
This will allow number
to be decoded, but you can also pass around instances of ExampleItem
where the default value is used.
You can also use it directly inside init
instead, during decoding:
struct ExampleItem: Decodable {
let number: Int
private enum CodingKeys: String, CodingKey {
case number
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
number = try container.decodeIfPresent(Int.self, forKey: .number) ?? 42
}
}
This will allow number
to be decoded, but use 42
as the default value if the decoding fails.
- Make the property a
var
, although you can also make it aprivate(set) var
:
struct ExampleItem: Decodable {
var number: Int = 42
}
Making it a var
will allow number
to be decoded, but it will also allow callers to modify it. By marking it as private(set) var
instead, you can disallow this if you want.
- Define an explicit
CodingKeys
enum:
struct ExampleItem: Decodable {
let number: Int = 42
private enum CodingKeys: CodingKey {}
}
This will prevent number
from being decoded. Since the enum has no cases, this makes it clear to the compiler that there are no properties that you want to decode.
This warning appears because immutable properties with initial values don't participate in decoding - after all, they're immutable and they have an initial value, which means that initial value will never be changed.
For example, consider this code:
struct Model: Decodable {
let value: String = "1"
}
let json = """
{"value": "2"}
"""
let decoder = JSONDecoder()
let model = try! decoder.decode(Model.self, from: json.data(using: .utf8)!)
print(model)
This will actually print Model(value: "1")
, even though the json we gave it had value
as "2"
.
In fact, you don't even need to provide the value in the data you're decoding, since it has an initial value anyway!
let json = """
{}
"""
let decoder = JSONDecoder()
let model = try! decoder.decode(Model.self, from: json.data(using: .utf8)!)
print(model) // prints "Model(value: "1")"
Changing the value to a var means it will decode correctly:
struct VarModel: Decodable {
var value: String = "1"
}
let json = """
{"value": "2"}
"""
let varModel = try! decoder.decode(VarModel.self, from: json.data(using: .utf8)!)
print(varModel) // "VarModel(value: "2")"
If you're seeing this error, it means your code has never correctly parsed the property in question when decoding. If you change it to a var, the property will be parsed correctly, which might be what you want - however, you should make sure that the data you're decoding always has that key set. For example, this will throw an exception (and crash since we're using try!
):
let json = """
{}
"""
let decoder = JSONDecoder()
struct VarModel: Decodable {
var value: String = "1"
}
let varModel = try! decoder.decode(VarModel.self, from: json.data(using: .utf8)!)
In conclusion, Xcode's suggestion is probably viable in many cases, but you should evaluate on a case by case basis whether changing the property to a var
will break your app's functionality.
If you want the property to always return the hard-coded initial value (which is what's happening right now), consider making it a computed property or a lazy var.
Solution: define an explicit CodingKeys
enum to prevent id
from decoded.
For example,
struct Course: Identifiable, Decodable {
let id = UUID()
let name: String
private enum CodingKeys: String, CodingKey {
case name
}
init(name: String) { self.name = name }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let name = try container.decodeIfPresent(String.self, forKey: .name)
self.name = name ?? "default-name"
}
}