-
-
Save mredig/f6d9efb196a25d857fe04a28357551a6 to your computer and use it in GitHub Desktop.
import Foundation | |
/// Makes an Encodable and Decodable properties encode to `null` instead of omitting the value altogether. | |
@propertyWrapper | |
public struct NullCodable<T> { | |
public var wrappedValue: T? | |
public init(wrappedValue: T?){ | |
self.wrappedValue = wrappedValue | |
} | |
} | |
extension NullCodable: Encodable where T: 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 NullCodable: Decodable where T: Decodable { | |
public init(from decoder: Decoder) throws { | |
let container = try decoder.singleValueContainer() | |
self.wrappedValue = try? container.decode(T.self) | |
} | |
} | |
struct WithNullCodable: Codable { | |
let value: String | |
@NullCodable var value2: String? | |
} | |
struct WithoutNullCodable: Codable { | |
let value: String | |
let value2: String? | |
} | |
let withA = WithNullCodable(value: "blah", value2: "bleh") | |
let withB = WithNullCodable(value: "blah", value2: nil) | |
let withoutA = WithoutNullCodable(value: "blah", value2: "bleh") | |
let withoutB = WithoutNullCodable(value: "blah", value2: nil) | |
func jsonString<Item: Encodable>(from item: Item) -> String { | |
let data = try! JSONEncoder().encode(item) | |
return String(data: data, encoding: .utf8)! | |
} | |
func object<Item: Decodable>(from string: String) -> Item { | |
let decoder = JSONDecoder() | |
let data = string.data(using: .utf8)! | |
return try! decoder.decode(Item.self, from: data) | |
} | |
let withAStr = jsonString(from: withA) | |
let withBStr = jsonString(from: withB) | |
print(withAStr) // {"value":"blah","value2":"bleh"} | |
print(withBStr) // {"value":"blah","value2":null} | |
let withoutAStr = jsonString(from: withoutA) | |
let withoutBStr = jsonString(from: withoutB) | |
print(withoutAStr) // {"value":"blah","value2":"bleh"} | |
print(withoutBStr) // {"value":"blah"} | |
let withADecoded: WithNullCodable = object(from: withAStr) | |
let withBDecoded: WithNullCodable = object(from: withBStr) | |
print(withADecoded) | |
print(withBDecoded) | |
let withoutADecoded: WithoutNullCodable = object(from: withoutAStr) | |
let withoutBDecoded: WithoutNullCodable = object(from: withoutBStr) | |
print(withoutADecoded) | |
print(withoutBDecoded) |
Lines 60 and 61 should have something like
// prints ....
Good idea!
Great example! Quick question is the Decodable
extension really needed? Isn't that a default behaviour of Decodable
optional properties?
It's been a while since I wrote this, so I cannot remember for sure why I put that in there, but I believe I wouldn't have included it unless it wasn't working without.
I suspect it might have something to do with flattening the wrapper of the content within NullCodable? I'd suggest experimenting. If you figure it out, I'll happily update with a comment explaining why it's there (as I should have initially!)
Thanks for quick answer! What do you mean by flattening the wrapper of the content
?
Without the singleValueContainer()
it might be trying to decode a value embedded within another container, like an array or dictionary.
Nice work!