I have a data model class with custom rules to deserialize from json. Specifically, the property flexibleProp can be defined in JSON as either String? or Int?. On my model class it should always be Int?. I have implemented a propertyWrapper to support custom deserialization rules:
class PersonInfo: Codable {
var regularProp: String?
@FlexibleInt varflexibleProp: Int?
init() {
}
}
@propertyWrapper struct FlexibleInt: Codable {
var wrappedValue: Int?
init() {
self.wrappedValue= nil
}
init(fromdecoder: Decoder) throws{
letcontainer = trydecoder.singleValueContainer()
ifletvalue = try? container.decode(String.self) {
wrappedValue= Int(value)
} elseifletintValue = try? container.decode(Int.self) {
wrappedValue= intValue
} else{
wrappedValue= nil
}
}
func encode(toencoder: Encoder) throws {
varcontainer = encoder.singleValueContainer()
trycontainer.encode(wrappedValue)
}
}
let decoder = JSONDecoder()
let payload1 = "{ \"flexibleProp\": \"123888\", \"regularProp\": \"qqq\" }"
letperson1= trydecoder.decode(PersonInfo.self, from: payload1.data(using: .utf8)!)
let payload2 = "{ \"flexibleProp\": \"\" }"
letperson2= trydecoder.decode(PersonInfo.self, from: payload2.data(using: .utf8)!)
let payload3 = "{ \"flexibleProp\": \"sss\" }"
letperson3= trydecoder.decode(PersonInfo.self, from: payload3.data(using: .utf8)!)
let payload4 = "{ }"
letperson4= trydecoder.decode(PersonInfo.self, from: payload4.data(using: .utf8)!) // FAILS HERE <------------
And it works in most cases correctly, only the last test case fails, when the flexibleProp property is completely missing in the json string (key and value are not defined):
▿ DecodingError
▿ keyNotFound : 2 elements
- .0 : CodingKeys(stringValue: "flexibleProp", intValue: nil)
▿ .1 : Context
- codingPath : 0 elements
- debugDescription : "No value associated with key CodingKeys(stringValue: \"flexibleProp\", intValue: nil) (\"flexibleProp\")."
- underlyingError : nil
I'm having no issues with the other String? property regularProp, when missing in json, it stays nil in the result, but for the flexibleProperty it fails to deserialize the whole object instead of just keeping the property nil. What is the best way to tell JSONDecoder to properly handle that property when it's not defined in the json string?
(Note: this answer is adapted from a similar post of mine on the Swift forums, explaining the behavior.)
Unfortunately, a property wrapper cannot influence
Codablesynthesis to allow a value for a specific key to be omitted entirely.When you write
the compiler generates the equivalent of
Because
_varflexiblePropis the "real" property (andvarflexiblePropis just a computed property), for the purposes of encoding and decoding, the compiler will encode and decode_varflexibleProp. This is crucial, because it's what allowsFlexibleIntto intercept encoding and decoding of the property with its ownCodableconformance at all.But it's important to note that the type of
_varflexiblePropisFlexibleInt, which is notOptional— it just contains anOptionalinside of it. This is important, becauseOptionalvalues are special forCodablesynthesis:Optionalproperty, it generates a call toencodeIfPresent(_:forKey:)/decodeIfPresent(_:forKey:), which skips encoding the value altogether if it isnil, and crucially, skips decoding the value altogether if the key is not present in the keyed containerOptionalproperty, it instead generates a call toencode(_:forKey:)/decode(_:forKey:), which unconditionally encode the value even ifnil, and unconditionally requires the key to be present on decodeIt's that last point that's problematic: because
_varflexiblePropis notOptional, the compiler attempts to initialize it asdecode(_:forKey:)has to check whether a key is present before anything can even be decoded, and so in your case, an error is thrown beforeFlexibleInt.init(from:)is ever called — because there isn't even any data for you to possibly decode from.If the compiler did try to use
decodeIfPresent(_:forKey:), it would have to provide some sort of default value for_varflexibleProp, which isn't always obvious how to do:Here, it might seem obvious that
_varflexiblePropshould be assigned a value ofFlexibleInt(), but the compiler has no way of knowing whether that has the semantics you want it to have (e.g., what if it has side effects?). In the linked Swift Forums thread, the property wrapper author didn't give it a zero-argument initializer, but instead had a single-argument initializer that took awrappedValue; this can very quickly devolve into a guessing game of how to initialize this non-Optionalproperty, which may not be safe for the compiler to do at all.Ultimately, this behavior isn't related to
JSONDecoderspecifically, but to how the compiler synthesizesCodableconformance. The workaround for now would be to implementPersonInfo.init(from:)directly, usingdecodeIfPresent(_:forKey:)yourself and initializing_varflexiblePropwith a default value.There's a bit more discussion in the thread about how this could possibly be handled in the future, so it's worth a read if you're curious. (And, now that macros are coming to Swift 5.9, it's possible that in the future,
Codableconformance could be pulled out of the compiler the way that it's written now, and instead implemented as a macro — and if so, toggles could be added to better influence how that happens.)