Summary: JSONEncoder
unexpectedly encodes nil values as null when working with generic types that wrap over a protocol.
Here's a short code that reproduces the problem:
protocol Event {
associatedtype Contents: Encodable
var name: String { get }
var contents: Contents { get }
}
struct ClickEvent: Event, Encodable {
let name: String = "click"
let contents: Int? = nil
}
struct EventWrapper<T: Event>: Encodable {
let name: String
let data: T.Contents
init(event: T) {
name = event.name
data = event.contents
}
}
let encoder = JSONEncoder()
let event = ClickEvent()
let wrapper = EventWrapper(event: event)
let encodedEventData = try! encoder.encode(event)
print(String(data: encodedEventData, encoding: .utf8)!)
// prints: {"name":"click"}
let encodedWrapperData = try! encoder.encode(wrapper)
print(String(data: encodedWrapperData, encoding: .utf8)!)
// prints: {"name":"click","data":null}
The problem here is the fact that the encoder encodes the nil value instead of skipping it. And it looks this is caused by the associated type of the Event
protocol.
How can I avoid this? Note that I cannot change the type hierarchy, as the actual code is part of a broader codebase. And the null
value is rejected by the backend, so I really need to get rid of it.
The JSONEncoder
class doesn't have any configuration options for nil values. Also, writing custom encode(to:)
methods is not possible since generics are involved. Are there any other options to fix this problem? Re-architecting the app or changing the backend code are unfortunately not feasible options...
CodePudding user response:
It's not actually impossible to manually write custom encode
methods in this case, since you just need to check if a "generic thing" is nil.
Using this answer, you can write:
enum CodingKeys: String, CodingKey {
case name, data
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
if case Optional<Any>.none = data as Any {
return
}
try container.encode(data, forKey: .data)
}
Alternatively, as I have learned here, if there are too many properties to encode, you can even write your own KeyedEncodingContainer
extension:
extension KeyedEncodingContainer {
public mutating func encode<T: Encodable>(_ value: T, forKey key: Key) throws {
if case Optional<Any>.none = value as Any {} else {
// even though value will not be nil at this point, we still use encodeIfPresent
// because calling encode will cause infinite recursion
try encodeIfPresent(value, forKey: key)
}
}
}
This "magically" works. The generated encode
implementation in EventWrapper
will resolve to this new implementation you wrote.