Search code examples
iosjsonswiftencodingcodable

Use Swift's Encodable to encode optional properties as null without custom encoding


I want to encode an optional field with Swift's JSONEncoderusing 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}

Solution

  • Let me suggest a property wrapper for this.

    @CodableExplicitNull

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

    Usage

    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)