I want to encode an optional field with Swift's JSONEncoder
using a struct
that conforms to the Encodable
protocol.
The default setting is that JSONEncoder
uses the encodeIfPresent
method, which means that values that are nil
are excluded from the Json.
How can I override this for a single property without writing my custom encode(to encoder: Encoder)
function, in which I have to implement the encoding for all properties (like this article suggests under "Custom Encoding" )?
Example:
struct MyStruct: Encodable {
let id: Int
let date: Date?
}
let myStruct = MyStruct(id: 10, date: nil)
let jsonData = try JSONEncoder().encode(myStruct)
print(String(data: jsonData, encoding: .utf8)!) // {"id":10}
Let me suggest a property wrapper for this.
import Foundation
@propertyWrapper
public struct CodableExplicitNull<Wrapped> {
public var wrappedValue: Wrapped?
public init(wrappedValue: Wrapped?) {
self.wrappedValue = wrappedValue
}
}
extension CodableExplicitNull: Encodable where Wrapped: Encodable {
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch wrappedValue {
case .some(let value): try container.encode(value)
case .none: try container.encodeNil()
}
}
}
extension CodableExplicitNull: Decodable where Wrapped: Decodable {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if !container.decodeNil() {
wrappedValue = try container.decode(Wrapped.self)
}
}
}
extension CodableExplicitNull: Equatable where Wrapped: Equatable { }
extension KeyedDecodingContainer {
public func decode<Wrapped>(_ type: CodableExplicitNull<Wrapped>.Type,
forKey key: KeyedDecodingContainer<K>.Key) throws -> CodableExplicitNull<Wrapped> where Wrapped: Decodable {
return try decodeIfPresent(CodableExplicitNull<Wrapped>.self, forKey: key) ?? CodableExplicitNull<Wrapped>(wrappedValue: nil)
}
}
struct Test: Codable {
@CodableExplicitNull var name: String? = nil
}
let data = try JSONEncoder().encode(Test())
print(String(data: data, encoding: .utf8) ?? "")
let obj = try JSONDecoder().decode(Test.self, from: data)
print(obj)
Gives
{"name":null}
Test(name: nil)