36

Running Xcode 12, my Swift 5 Xcode project now has warnings whenever a Decodable or Codable type declares a let constant with an initial value.

struct ExampleItem: Decodable {
    let number: Int = 42 // warning
}

Immutable property will not be decoded because it is declared with an initial value which cannot be overwritten

Xcode suggests changing the let to a var:

Fix: Make the property mutable instead

    var number: Int = 42

It also suggests the fix:

Fix: Set the initial value via the initializer or explicitly define a CodingKeys enum including a 'title' case to silence this warning

What is the purpose of this new warning? Should it be heeded, or ignored? Can this type of warning be silenced?

Should Xcode's fix be implemented? Or is there a better solution?

pkamb
  • 33,281
  • 23
  • 160
  • 191

5 Answers5

54

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:


  1. 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.


  1. Make the property a var, although you can also make it a private(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.


  1. 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.

pkamb
  • 33,281
  • 23
  • 160
  • 191
Suyash Srijan
  • 629
  • 5
  • 8
  • 3
    Great to have an answer from the person who implemented the new error. Can I ask how you found this question so quickly, and with a new SO account?! – pkamb Jun 24 '20 at 16:27
  • 1
    @pkamb I saw a link to this question on Twitter. I didn’t have a S/O account already, so I created one and then left an answer. – Suyash Srijan Jun 25 '20 at 01:18
  • What is the simplest way of silencing the warning in the following case? (id isn't in the json file.) struct JsonCard: Codable { let id = UUID() let a: String let b: String } – Hugo F Sep 19 '20 at 10:31
  • 1
    @HugoF You can declare the CodingKeys enum explicitly & exclude the id property. – Suyash Srijan Sep 19 '20 at 16:37
  • 7
    Are any considerations made to disable this warning? There are cases where is this desirable, but now we are seeing warnings for valid code. We understand how Codable works, and would like to have a way to use correct swift code without having warning. (I can draw parallels to discardableResult that allows the caller to ignore the result without generating a warning) Writing your own coding keys is *far* more error prone than relying on compiler generated code. (In general fewer lines of code is always better) – Sam Oct 17 '20 at 20:41
  • @Sam There are multiple ways to silence this warning as described above and explicitly writing the CodingKeys is just one of them. Swift does not offer a general way to silence a particular warning. – Suyash Srijan Oct 18 '20 at 13:50
  • 2
    I see that, however the behavior we desire is that the value gets encoded into the json, but doesn't get decoded. It makes a lot of sense how Codable works if you thing about how the auto generated code will would look like. But what irks me is that we are left with no option for compiler generated code to follow a quite common convention for heterogenous json arrays. (I hope I'm not sounding too ranty ) – Sam Oct 18 '20 at 20:53
  • 3
    tbh writing the CodingKeys is not that satisfactory. Usually silencing a warning involves doing something as simple as adding `as Any` or adding brackets around something. Writing out the CodingKeys is more complex and means a field added in the future could get forgotten about. If swift had a proper system for suppressing warnings this would be fine. Now I've got to write out the CodingKeys for about 70 structs... – Jonathan. Oct 28 '20 at 18:18
  • 5
    our use case being that we have structs that are Codable, when Encoding the values should always be a specific string, when decoding it doesn't matter, the value still need to be the same, so actually we can't omit them from CodingKeys because then they won't be encoded. I really don't think a warning should be here when it's just informational to the developer and there's no real way to suppress it – Jonathan. Oct 28 '20 at 18:35
  • 7
    I agree. I have a use case where I do not want this warning there. `let id = UUID()` The struct contains a lot more variables in it. It seems odd that I have to explicity create init/or enums just to silence something that i need... Maybe handle it another way? – Just a coder Nov 30 '20 at 23:59
  • 3
    Oh, this was obviously a wrong decision. Why should we have a warning for a completely valid and working code? – Sulthan Dec 21 '20 at 21:25
  • 2
    So I implemented one of the "fixes" by converting let to var. All compiled OK but found an issue later down the line associated with Realm objects (vars & collections)which actually broke the functionality of the app. So yes implement the fixes but be very careful as they can have side effects. – wuf810 Feb 09 '21 at 17:00
  • 3
    Just adding here too. This is a very silly warning to have, especially now that we need our Codables to also conform to Identifiable. Maybe a property wrapper needs to be created for this. Even though LET should be more than enough.... – Beau Nouvelle May 26 '21 at 04:32
  • I like the 1private (set) var` approach –  Oct 08 '21 at 01:37
7

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.

Noah Gilmore
  • 1,319
  • 2
  • 15
  • 24
4

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"
  }
}
Harry Zhang
  • 799
  • 9
  • 6
3

To suppress the warning you can use a computed property instead:

struct ExampleItem: Decodable {
    var number: Int { 42 } // no warning anymore
}

The behavior stays the same - the decoder won't mess with the number field. And you don't need to implement custom CodingKeys.

pkamb
  • 33,281
  • 23
  • 160
  • 191
Mike Keskinov
  • 11,614
  • 6
  • 59
  • 87
2

The suggested workarounds by @SuyashSrijan suppresses the warning but may also lead to further developer errors. I've written an alternative work around here:

public struct IdentifierWrapper<T>: Identifiable {
    public let id = UUID()
    public let value: T
}

Usage:

struct Model: Codable, Identifiable {
    public let name: String
}

let wrapper = IdentifierWrapper(value: Model(name: "ptrkstr"))
Patrick
  • 2,035
  • 17
  • 27