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}
heyfrank
  • 5,291
  • 3
  • 32
  • 46
  • 3
    Related question but using custom encoding logic instead: https://stackoverflow.com/questions/47266862/encode-nil-value-as-null-with-jsonencoder/47268112 – Paulo Mattos Nov 14 '17 at 17:27
  • 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)`. – Patru May 20 '18 at 13:40
  • My API resets values by setting `null` explicitly on them. And from my experience, is not a rare case... – heyfrank May 20 '18 at 15:32
  • 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. – Rob Napier May 23 '18 at 00:42

4 Answers4

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

Lex
  • 300
  • 1
  • 11
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)
heyfrank
  • 5,291
  • 3
  • 32
  • 46
Paul B
  • 3,989
  • 33
  • 46
  • 1
    Best solution so far. Can you explain why we need the extension on `KeyedDecodingContainer` ? – heyfrank Mar 27 '23 at 09:12
  • 1
    @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. – Paul B Apr 02 '23 at 09:12
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")])
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
highFly
  • 95
  • 1
  • 5
-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.

Patru
  • 4,481
  • 2
  • 32
  • 42