Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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}
like image 553
heyfrank Avatar asked Nov 14 '17 16:11

heyfrank


People also ask

How do you handle null in Codable Swift?

A null value (no string) is treated as nil by default so the decoding is supposed to succeed if the property is optional. By the way: You can omit the CodingKeys. If the name of the properties are the same as the keys you don't need explicit CodingsKeys .

What is encodable and decodable in Swift?

The Codable protocol in Swift is really a union of two protocols: Encodable and Decodable . These two protocols are used to indicate whether a certain struct, enum, or class, can be encoded into JSON data, or materialized from JSON data.

What is Codingkey Swift?

A type that can be used as a key for encoding and decoding.


1 Answers

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

like image 133
Lex Avatar answered Sep 28 '22 08:09

Lex