How do I make a type that only successfully decodes from the empty object `{}`?

I had assumed that this task would be simple before I started, but it has stumped me so far, and any help would be much appreciated.

I have been using this type:

struct EmptyCodable: Codable { }

If I encode it with a JSONEncoder I get what I want, namely, the empty object: {}

If I try to reverse the process with a JSONDecoder it successfully produces an instance of EmptyCodable.

What my EmptyCodable type doesn't do is fail to decode a non-empty object.

For example, this test fails but should pass:

func test_decodingFromNonEmptyObject () {
    struct Foo: Codable { var bar: Bool } 
    do {
        let nonEmptyObjectData = try JSONEncoder().encode(Foo(bar: true))

        /// This should throw...
        try JSONDecoder().decode(EmptyCodable.self, from: nonEmptyObjectData)

        /// ...so this should be skipped
        XCTFail()
    } catch { }
}

I have tried everything I can think of, I've googled it, I've read the comments on this Swift forums thread, and I've pondered whether I may be being blocked because the thing I think I want to do somehow doesn't make sense. None of these things have satisfied me so I'm posting this.

As far as why I want this, it is because today I was not alerted to the fact that a REST request that I was making was actually not returning an empty object in the payload as I believed it would, but rather was returning some populated object that indicated an error. Had I been able to enforce that the payload was nothing more than the empty object {} I would have been alerted that requests to this endpoint were actually failing. Of course I can and perhaps should rely on more reliable ways to check the binary "success"/"failure" status of a response, but it still seems like this should be a concept that one can express in Swift.

Doing this is relatively straightforward, by implementing init(from:) yourself, and checking the decoder for a container keyed by a CodingKey type which can take on any value — if the container has valid keys, you know it's not empty.

struct EmptyCodable: Codable {
    private struct AnyCodingKey: CodingKey {
        let intValue: Int?
        let stringValue: String
        
        init?(intValue: Int) {
            self.intValue = intValue
            stringValue = "\(intValue)"
        }

        init?(stringValue: String) {
            intValue = Int(stringValue)
            self.stringValue = stringValue
        }
    }

    init(from decoder: Decoder) throws {
        // Since `AnyCodingKey` can take any `String` or `Int` value,
        // if there's a keyed container, you're guaranteed to read from
        // it successfully.
        let container = try decoder.container(keyedBy: AnyCodingKey.self)
        guard container.allKeys.isEmpty else {
            // Or whatever you want:
            throw DecodingError.dataCorrupted(
                DecodingError.Context(
                    codingPath: decoder.codingPath,
                    debugDescription: "Expected empty object, but received object with keys: \(container.allKeys)"
                )
            )
        }
    }
}

This gives the behavior that you expect:

let nonEmptyObjectData = try JSONEncoder().encode(Foo(bar: true))
try JSONDecoder().decode(EmptyCodable.self, from: nonEmptyObjectData)
// => Swift/ErrorType.swift:200: Fatal error: Error raised at top level: Swift.DecodingError.dataCorrupted(Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected empty object, but received object with keys: [AnyCodingKey(stringValue: \"bar\", intValue: nil)]", underlyingError: nil))

This approach can also extend to allowing EmptyCodable to be decoded from an empty array, too (by catching a .typeMismatch error on try decoder.container(keyedBy: AnyCodingKey.self) and attempting to get an UnkeyedDecodingContainer — if it succeeds, you confirm that the container isAtEnd and otherwise throw a similar error.

1 Like

Thank you! I have used the CodingKey protocol in the past, but it always worked so well for my simple coding customization needs that until now I had never needed to deepen my understanding of the real way that it interacts with Codable. Now having jumped to its definition and having seen your simple implementation of this use case, I think I've taken the next step in my understanding of CodingKey and it's all quite clear to me now :+1::pray:

1 Like

Why does AnyKey need to actually store the string/integer?

1 Like

You're right, it doesn't. Here is the implementation I'm now using:

///
private struct AnyCodingKey: CodingKey {
    
    ///
    var intValue: Int? { nil }
    var stringValue: String { "" }
    
    ///
    init?(intValue: Int) { }
    init?(stringValue: String) { }
}

Ah, it doesn’t — but AnyCodingKey as written can be a handy utility in other contexts, so I copied it exactly from a snippet I have. You can indeed write an EmptyCodingKey or similar as @jeremyabannister shows.

1 Like