2

How can I encode a nil property as an empty JSON object?

struct Foo: Encodable {
    let id = 10
    let bar: Bar? = nil
}

struct Bar: Encodable {
    let number: Int
}

let data = try! JSONEncoder().encode(Foo())

print(String(data: data, encoding: .utf8)!)

This prints out:

"{"id":7}"

What I want is:

"{"id":7, "bar":{}}"

5
  • This would basically break the symmetry between encoding and decoding, wondering what's the use case that would demand such an encoding. Commented Sep 27, 2021 at 16:30
  • let bar: Bar? = nil this is pointless. bar will always be nil Commented Sep 27, 2021 at 17:23
  • Wouldn't be better to encode NSNull instead ? Commented Sep 27, 2021 at 17:24
  • @LeoDabus this code is just to showcase my problem. Commented Sep 28, 2021 at 9:36
  • 1
    @Cristik the API I'm using requires that I do so. I agree that it's not conventional, but I don't have a choice. And I'm only encoding it, never decoding it from JSON. Commented Sep 28, 2021 at 9:38

3 Answers 3

3

You can introduce an empty struct with no properties to the encoder, when bar = nil

struct Foo: Encodable {
    let id = 10
    let bar: Bar? = nil
    
    enum CodingKeys : String, CodingKey {
        case id
        case bar
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(id, forKey: .id)
        
        if let bar = bar {
            try container.encode(bar, forKey: .bar)
        }
        else {
            try container.encode(Empty(), forKey: .bar)
        }
    }
}

struct Bar: Encodable {
    let number: Int
}

struct Empty: Encodable {
}
Sign up to request clarification or add additional context in comments.

2 Comments

You got there before me. LOL! Was just typing out an answer pretty much exactly as this too. 🤣
@Fogmeister I guess I was few seconds quicker to post the answer then :)
2

Implement a custom encode(to:) for Foo and use an empty dictionary if Bar is nil

struct Foo: Encodable {
    let id = 10
    let bar: Bar? = nil

    enum CodingKeys: String, CodingKey {
        case id, bar
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(id, forKey: .id)
        switch bar {
        case .some(let value):
            try container.encode(value, forKey: .bar)
        case .none:
            try container.encode([String: Bar?](), forKey: .bar)
        }
    }
}

Comments

1

Not sure why you'd need this, as encoding to a form that fails decoding is not something usually done.

Nonetheless, if you find yourself needing this kind of logic in multiple places, you can extend KeyedEncodingContainer with this kind of functionality:

extension KeyedEncodingContainer {
    mutating func encodeOptional<T: Encodable>(_ value: T?, forKey key: Self.Key) throws {
        if let value = value { try encode(value, forKey: key) }
        else { try encode([String:String](), forKey: key) }
    }
}

, and then implement the encode(to:) method in Foo:

struct Foo: Encodable {
    let id = 10
    let bar: Bar? = nil
    
    enum CodingKeys: String, CodingKey {
        case id
        case bar
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(id, forKey: .id)
        try container.encodeOptional(bar, forKey: .bar)
    }
}

You can also extend UnkeyedDecodingContainer, and SingleValueDecodingContainer with similar encodeOptional methods, if you find yourself needing to encode empty JSON objects for nil values in other kind of containers.

1 Comment

This looks very clean! The only reason I need this is because the API I'm using requires that I do so...

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.