-
-
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) |
Nice work!
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.
Lines 60 and 61 should have something like
// prints ....