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

4

17

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}
Insomnolence answered 14/11, 2017 at 16:22 Comment(4)
Related question but using custom encoding logic instead: #47267362Sectarianism
What exactly are you trying to achieve? A JSON entry in the hash such as "date": null;? What difference do you intend to convey by making the null explicit? If you plan to consume the result using Swift you will have a really hard time to tell the difference in the first place. Your link seems to be the only notable reference to encodeIfPresent, but the case seems to be sufficiently rare to merit the implementation of encode(to encoder: Encoder).Nimbostratus
My API resets values by setting null explicitly on them. And from my experience, is not a rare case...Insomnolence
I don't believe this is possible without implementing your own encode. (The pieces of JSONEncoder you'd need to override are fileprivate.) If it is non-trivial to implement, I would recommend SwiftGen to write it for you; this should be straightforward to build in SwiftGen. As a rule, it is not possible to get semi-custom Encodables. There are a small number of very specific configuration points, but beyond that, it's currently default or custom. I expect this to improve.Fidelfidela
S
1

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)
Subalternate answered 23/3, 2023 at 14:14 Comment(2)
Best solution so far. Can you explain why we need the extension on KeyedDecodingContainer ?Insomnolence
@heyfrank, the KeyedDecodingContainer stuff allows for compatibility with regular Optional properties coded as usual, i.e. omitted entirely. Simply speaking it allows for implicit nulls too when decoding.Subalternate
M
3
import Foundation

enum EncodableOptional<Wrapped>: ExpressibleByNilLiteral {
    case none
    case some(Wrapped)
    init(nilLiteral: ()) {
        self = .none
    }
}

extension EncodableOptional: Encodable where Wrapped: Encodable {

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        switch self {
        case .none:
            try container.encodeNil()
        case .some(let wrapped):
            try wrapped.encode(to: encoder)
        }
    }
}

extension EncodableOptional{

    var value: Optional<Wrapped> {

        get {
            switch self {
            case .none:
                return .none
            case .some(let v):
                return .some(v)
            }
        }

        set {
            switch newValue {
            case .none:
                self = .none
            case .some(let v):
                self = .some(v)
            }
        }
    }
}

struct User: Encodable {
    var name: String
    var surname: String
    var age: Int?
    var gender: EncodableOptional<String>
}

func main() {
    var user = User(name: "William", surname: "Lowson", age: 36, gender: nil)
    user.gender.value = "male"
    user.gender.value = nil
    print(user.gender.value ?? "")
    let jsonEncoder = JSONEncoder()
    let data = try! jsonEncoder.encode(user)
    let json = try! JSONSerialization.jsonObject(with: data, options: [])
    print(json)

    let dict: [String: Any?] = [
        "gender": nil
    ]
    let d = try! JSONSerialization.data(withJSONObject: dict, options: [.prettyPrinted])
    let j = try! JSONSerialization.jsonObject(with: d, options: [])
    print(j)
}

main()

This will give you output after executing main:

{
    age = 36;
    gender = "<null>";
    name = William;
    surname = Lowson;
}
{
    gender = "<null>";
}

So, you can see that we encoded gender as it'll be null in dictionary. The only limitation you'll get is that you'll have to access optional value via value property

Magnific answered 4/11, 2018 at 15:54 Comment(0)
S
1

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)
Subalternate answered 23/3, 2023 at 14:14 Comment(2)
Best solution so far. Can you explain why we need the extension on KeyedDecodingContainer ?Insomnolence
@heyfrank, the KeyedDecodingContainer stuff allows for compatibility with regular Optional properties coded as usual, i.e. omitted entirely. Simply speaking it allows for implicit nulls too when decoding.Subalternate
C
0

You can use something like this to encode single values.

struct CustomBody: Codable {
    let method: String
    let params: [Param]

    enum CodingKeys: String, CodingKey {
        case method = "method"
        case params = "params"
    }
}

enum Param: Codable {
    case bool(Bool)
    case integer(Int)
    case string(String)
    case stringArray([String])
    case valueNil
    case unsignedInteger(UInt)
    case optionalString(String?)

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let x = try? container.decode(Bool.self) {
            self = .bool(x)
            return
        }
        if let x = try? container.decode(Int.self) {
            self = .integer(x)
            return
        }
        if let x = try? container.decode([String].self) {
              self = .stringArray(x)
              return
          }
        if let x = try? container.decode(String.self) {
            self = .string(x)
            return
        }
        if let x = try? container.decode(UInt.self) {
            self = .unsignedInteger(x)
            return
        }
        throw DecodingError.typeMismatch(Param.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for Param"))
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        switch self {
        case .bool(let x):
            try container.encode(x)
        case .integer(let x):
            try container.encode(x)
        case .string(let x):
            try container.encode(x)
        case .stringArray(let x):
            try container.encode(x)
        case .valueNil:
            try container.encodeNil()
        case .unsignedInteger(let x):
            try container.encode(x)
        case .optionalString(let x):
            x?.isEmpty == true ? try container.encodeNil() : try container.encode(x)
        }
    }
}

And the usage is like this

RequestBody.CustomBody(method: "WSDocMgmt.getDocumentsInContentCategoryBySearchSource", 
                       params: [.string(legacyToken), .string(shelfId), .bool(true), .valueNil, .stringArray(queryFrom(filters: filters ?? [])), .optionalString(sortMethodParameters()), .bool(sortMethodAscending()), .unsignedInteger(segment ?? 0), .unsignedInteger(segmentSize ?? 0), .string("NO_PATRON_STATUS")])
Clutch answered 21/3, 2020 at 9:56 Comment(0)
N
-1

If you try to decode this JSON your trusty JSONDecoder will create exactly the same object as exemplified in this Playground:

import Cocoa

struct MyStruct: Codable {
    let id: Int
    let date: Date?
}

let jsonDataWithNull = """
    {
        "id": 8,
        "date":null
    }
    """.data(using: .utf8)!

let jsonDataWithoutDate = """
    {
        "id": 8
    }
    """.data(using: .utf8)!

do {
    let withNull = try JSONDecoder().decode(MyStruct.self, from: jsonDataWithNull)
    print(withNull)
} catch {
    print(error)
}

do {
    let withoutDate = try JSONDecoder().decode(MyStruct.self, from: jsonDataWithoutDate)
    print(withoutDate)
} catch {
    print(error)
}

This will print

MyStruct(id: 8, date: nil)
MyStruct(id: 8, date: nil)

so from a "standard" Swift point of view your distinction makes very little sense. You can of course determine it, but the path is thorny and leads through the purgatory of JSONSerialization or [String:Any] decoding and a lot more ugly optionals. Of course if you are serving another language with your interface that might make sense, but still I consider it a rather rare case which easily merits the implementation of encode(to encoder: Encoder) which is not hard at all, just a little tedious to clarify your slightly non-standard behaviour.

This looks like a fair compromise to me.

Nimbostratus answered 22/5, 2018 at 20:27 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.