Lifting the requirement that properties of a `Decodable` type must also be `Decodable` to synthesize `init(from:)`

In this package which uses property wrappers to affect the how types are decoded, Resilient is a struct which is Decodable but is never actually intended to be initialized via init(from:) (indeed, that codepath currenly asserts). I would be able to remove this assertion, conformance and initializer if the synthesized Decodable initializer didn't require that the type of the property be Decodable, only that the synthesized call to container.decode(Foo.self, forKey: .foo) type checked correctly. We would then be able to catch these asserts at compile-time, and not need to provide a fallback path.

What do folks think?

1 Like

What do they use instead? More importantly, how would the synthesizer know to use that method?

This seems somewhat backwards to me...

Would you not want to prevent using this property wrapper with types which "should not" be used with it?

I.e. rather than Value: Codable it seems you'd want to offer it for more specific types, where Value: Optional where Wrapped: Codable specifically? If I understand what you're after here: you want to allow "fallback to nil if fails" right? So how about pushing the failure from runtime to compile time, when you know the construction is wrong?

The correct method is selected from the overloads of decode defined on KeyedDecodingContainer. This uses the basic Swift overload-resolution mechanisms, so if I were to define a decode(MyNotDecodableType.self, forKey: Key) on KeyedDecodingContainer, it would happily call that method whether or not MyNotDecodableType conformed to Decodable or not. CodableWrappers uses a similar approach.

Ok, I looked at the package. I'm surprise that it works. Of course the synthesiser uses the concrete type and so can figure out the right "overload".

Anyhow, I suppose it could be technically possible (discarding the compatibility issues) to change the checking of T: Codable to KeyedValueContainer.decode(T.self, ...).

I agree with @ktoso that you're allowing more conformance much more than it should, that Resilient<Int> is Codable when it shouldn't. Though I couldn't think of a way to easily express it in the current type system.

Further, I'd expect to be able to decode [Resilient<T>], and top-level Resilient<T>, successfully, which would still go through Resilient.init(from:).

Yeah, the reason that I explicitly assert when decoding things like [Resilient<T>], is that the behavior is no different from just decoding [T] so it is very likely the developer made a mistake. This is the kind of semantics we would be able to model if the synthesized init(from:) tried to type check the synthesized function calls rather than just verifying that the property was Decodable.

1 Like

I recently stumbled on another example where this would be useful:

If the Bar line is uncommented, it doesn't compile but if you look at "z", it doesn't even decode that value, it uses the constant.

Recent development snapshot toolchains have actually added a warning for this situation:

❱ TOOLCHAINS=org.swift.50202004131a swiftc -c - <<'EOF'
struct Foo: Decodable {
  let y: Int
  let z: Int = 42

<stdin>:3:7: warning: immutable property will not be decoded because it is declared with an initial value which cannot be overwritten
  let z: Int = 42
<stdin>:3:7: note: set the initial value via the initializer or explicitly define a CodingKeys enum without a 'z' case to silence this warning
  let z: Int = 42
<stdin>:3:7: note: make the property mutable instead
  let z: Int = 42
  ~~~ ^