I think this is an excellent start on a list of requirements for improving our ability to customize the synthesized Codable implementations. I think user-defined attributes is the right approach to improving Codable synthesis and customization. However, I don't think property wrappers is the right way to go about that.
Property wrappers are only available on properties. They are not available on types, enum cases or associated values. I wrote a large Sourcery template that synthesizes Codable implementations for my team. The encodings we have to support require the ability to annotate all of the above mentioned declarations. Property wrappers simply don't provide a general enough foundation to build a robust facility for customizing Codable synthesis.
Any facility that makes it into the language should be able to support a broad range of customized encodings. Instead of using property wrappers, we should have general user-defined attributes and contracts that allow them to participate in Codable synthesis (perhaps similar in nature to the property wrappers contract, but oriented specifically at encodings). Rust's serde uses an approach along these lines. @Chris_Lattner3 has previously described something similar for Swift.
Yes, property wrappers can add overhead. How much overhead depends on the wrapper. But generally the overhead is probably going to be negligible for simple wrappers.
I don't believe anything I've done for CodableWrappers will have a meaningful performance impact. I'm intending to do some performance tests at some point, but it's possible it would be more performant than customizing JSON(En/De)coder since it doesn't require the extra options checks.
If/when we gain the ability for users to customize code generation that's definitely the way to go. Maybe it's better to keep it as a 3rd party library till then.
Would there be problems with adding the options as Property Wrappers now, and then changing it internally once that capability is added? If the syntax was decided it would be source stable and whether it used a Property Wrapper or some future feature could be an implementation detail. Since Property Wrapper generates code I'm guessing there wouldn't be any ABI issues.
The extendability would probably have to be dropped, but that's still a win in my book.
Yes. Property wrappers are intrusive to the model. This affects layout, access control and runtime behavior. I don't think we can adopt this model and later keep the same syntax but completely change the semantics.
I think it's fine if people want to use libraries of property wrappers that allow customization of Codable in their own code. But I don't think it's something we should accept into the standard library or something that Apple should add to Foundation.
Would there be problems with adding the options as Property Wrappers now, and then changing it internally once that capability is added?
A perfect example of why this wouldn't work is the mutability/immutability discussion above. With a pure attribute-based approach, the property's mutability is directly determined by the declaration of let vs var... Whereas the property wrapper approach can't help but interfere in the native Swift grammar for property mutability because of the inherent indirection at the heart of its implementation.
Thinking it through more, there would likely have to be changes to the mutability issues as @karim reminded me to even remain properly source stable.
Regarding any layout/runtime changes, I think I'm beginning to grasp the issue. My understanding of @propertyWrapper is that once compiled it's just a Property of the Type with no special behavior/layout.
So once a simple @propertyWrapper struct Wrapper (with none of the protocol/generic business that's in my library) is compiled it's backwards deployable and valid unless/until the ABI changes. If later an attribute/code-generation-annotation was later used to replace it code compiled by either version would still be valid.
However since a @propertyWrapper also exposes a Type, it can be used directly and/or used elsewhere so removing it would break things. So to use this model that Type information would have to somehow be removed or not exposed, which AFAIK would require new language features
I guess it will probably have to wait till those features are added or rejected.
well that puts a major wrench in this whole thing. Hopefully I can find a workaround but it doesn't look promising... Any idea if this is viewed as a bug that will be fixed?
Echoing previous sentiment about invasiveness and tight coupling to the model, what happens if I want my type to be encodable and decodable in multiple formats? For example, JSON and XML?
The value of this approach is it works for any encoder/decoder. As you can see in the Unit Tests the same customization works for both JSONEncoder and PropertyListEncoder.
It wouldn't work for different behaviors between formats, but that's a different (less common) use case than what I'm trying to solve. Though when Property Wrapper composition is fixed it would be possible to have one decoding and a different decoding.
I'm not sure it can be qualified as a "bug", because backward compatibility, for both successes and failures, is paramount for Decodable. There may be ways to improve the situation, though, by allowing init(from decoder: Decoder) to run even if the key is missing, and thus allow property wrappers to handle this particular case. Before I would jump on this wagon, though, I would read all posts by @itaiferber in order to become very intimate with the distribution of responsibilities in the design of Codable.
Now I welcome your exploration, and find CodableWrappers inspiring, but I agree with you: this is a major hindrance.
This does not mean one can't enjoy CodableWrappers as a third-party library. As the author of the library, you are responsible for its limitations, though.
Data with a missing key always fails ("keyNotFound") to decode a property wrapper. My expectation is this should behave just like a normal Optional property, but it does not. At the very least, it shouldn't fail with apparently no possible workaround. That's why this seems to me like a bug.
Though seemingly a bug without an obvious fix so it's understandable that it either fell through the cracks or wasn't seen as critical. And now that it's shipped...changing behavior gets more risky.
I've worked a bit with a custom encoder and my main take away was how generalized things need to be, so it seems like changes at the encoder level should be avoided.
My naive solution is that it could be handled by the generated Decodable implementation of containing Type, but I'm not aware of the details of how that works so don't know whether that fits well with how that feature is designed. It also wouldn't effect shipped code directly. Whether that's a win or a loss...
So the question is: can we extend Decoder so that it becomes possible to 1. enter Decodable.init(from:) even if the key is missing (so that the property wrapper can be instantiated), and 2. allow this init(from:) to notice that it is decoding from a missing value and react accordingly? All of this in a 100% backward compatible way?
The answer to this question lies there. If it's possible, then this would give a great pitch.
Resurrect ExpressibleByNilLiteral and have Decoder detects that instead.
Find a way to express Wrapper<Value>?. Maybe @Wrapper? var value: Value
1 is already discouraged, and I do agree.
2 seems to be filling the gap in property wrapper, though I do question its usefulness outside of code synthesis.
I understand the observed behavior and why it happens. My point is from a User POV wrapping a property shouldn't break decoding.
My concern with that is it's not done by Decodable, it's done by the implementation. I believe a custom version could already do this if desired, (besides the fact that it may break things). I don't think trying to force every Decoder implementation to use a specific approach is good solution.