Encode nil value as null with JSONEncoder Encode nil value as null with JSONEncoder ios ios

Encode nil value as null with JSONEncoder


Yes, but you'll have to write your own encode(to:) implementation, you can't use the auto-generated one.

struct Foo: Codable {    var string: String? = nil    var number: Int = 1    func encode(to encoder: Encoder) throws {        var container = encoder.container(keyedBy: CodingKeys.self)        try container.encode(number, forKey: .number)        try container.encode(string, forKey: .string)    }}

Encoding an optional directly will encode a null, like you're looking for.

If this is an important use case for you, you may consider opening a defect at bugs.swift.org to ask for a new OptionalEncodingStrategy flag to be added on JSONEncoder to match the existing DateEncodingStrategy, etc. (See below why this is likely impossible to actually implement in Swift today, but getting into the tracking system is still useful as Swift evolves.)


Edit: To Paulo's questions below, this dispatches to the generic encode<T: Encodable> version because Optional conforms to Encodable. This is implemented in Codable.swift this way:

extension Optional : Encodable /* where Wrapped : Encodable */ {    @_inlineable // FIXME(sil-serialize-all)    public func encode(to encoder: Encoder) throws {        assertTypeIsEncodable(Wrapped.self, in: type(of: self))        var container = encoder.singleValueContainer()        switch self {        case .none: try container.encodeNil()        case .some(let wrapped): try (wrapped as! Encodable).__encode(to: &container)        }    }}

This wraps the call to encodeNil, and I think letting stdlib handle Optionals as just another Encodable is better than treating them as a special case in our own encoder and calling encodeNil ourselves.

Another obvious question is why it works this way in the first place. Since Optional is Encodable, and the generated Encodable conformance encodes all the properties, why does "encode all the properties by hand" work differently? The answer is that the conformance generator includes a special case for Optionals:

// Now need to generate `try container.encode(x, forKey: .x)` for all// existing properties. Optional properties get `encodeIfPresent`....if (varType->getAnyNominal() == C.getOptionalDecl() ||    varType->getAnyNominal() == C.getImplicitlyUnwrappedOptionalDecl()) {  methodName = C.Id_encodeIfPresent;}

This means that changing this behavior would require changing the auto-generated conformance, not JSONEncoder (which also means it's probably really hard to make configurable in today's Swift....)


Here's an approach that uses a property wrapper (requires Swift v5.1):

@propertyWrapperstruct NullEncodable<T>: Encodable where T: Encodable {        var wrappedValue: T?    init(wrappedValue: T?) {        self.wrappedValue = wrappedValue    }        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()        }    }}

Sample usage:

struct Tuplet: Encodable {    let a: String    let b: Int    @NullEncodable var c: String? = nil}struct Test: Encodable {    @NullEncodable var name: String? = nil    @NullEncodable var description: String? = nil    @NullEncodable var tuplet: Tuplet? = nil}var test = Test()test.tuplet = Tuplet(a: "whee", b: 42)test.description = "A test"let data = try JSONEncoder().encode(test)print(String(data: data, encoding: .utf8) ?? "")

Output:

{  "name": null,  "description": "A test",  "tuplet": {    "a": "whee",    "b": 42,    "c": null  }}

Full implementation here: https://github.com/g-mark/NullCodable


Here is an approach we have used in a project. Hope it helps.

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 something 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")])