Skip to content

Instantly share code, notes, and snippets.

@mredig
Last active October 19, 2023 12:04
Show Gist options
  • Save mredig/f6d9efb196a25d857fe04a28357551a6 to your computer and use it in GitHub Desktop.
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)
@mredig
Copy link
Author

mredig commented Feb 15, 2022

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!)

@dziobaczy
Copy link

Thanks for quick answer! What do you mean by flattening the wrapper of the content?

@mredig
Copy link
Author

mredig commented Feb 15, 2022

Without the singleValueContainer() it might be trying to decode a value embedded within another container, like an array or dictionary.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment