JSONDecoder fails when passing implicitly unwrapped optional Data

This one really threw me off.
I was attempting to decode some JSON with Codable and JSONDecoder.

import XCTest
import Foundation

final class DecodingTests: XCTestCase {

    struct TestObject: Decodable, Equatable {
        let value: Int
    }

    func testGroupResponseDecodedWithoutError() throws {
        let jsonString = "{\"value\":1}"
        let implicitlyUnwrappedData: Data! = jsonString.data(using: .utf8)
        let concreteData: Data = jsonString.data(using: .utf8)!

        // The Data instances are equivalent
        XCTAssertEqual(implicitlyUnwrappedData, concreteData)

        let objectFromConcreteData = try JSONDecoder().decode(TestObject.self, from: concreteData)
        let objectFromImplicitData = try JSONDecoder().decode(TestObject.self, from: implicitlyUnwrappedData) // key not found: "value"

        XCTAssertEqual(objectFromConcreteData.value, 1)
        XCTAssertEqual(objectFromImplicitData.value, 1)
        XCTAssertEqual(objectFromConcreteData, objectFromImplicitData)
    }
}

Attempting to decode the object from the implicitly unwrapped Data instance throws an error:

caught error: "keyNotFound(CodingKeys(stringValue: "value", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"value\", intValue: nil) (\"value\").", underlyingError: nil))"

Despite the underlying data being exactly the same as the other Data instance.
When stepping through a manually implemented decode function, the container's allKeys property is empty.

struct TestObject: Decodable, Equatable {
    let value: Int

    enum CodingKeys: String, CodingKey {
        case value
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        value = try container.decode(Int.self, forKey: .value)
    }
}

Especially if passing a nil value into the decode function, I would expect the Thread 1: Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value error to be thrown.
But this doesn't happen even in the following scenario:

var nilData: Data!
_ = try JSONDecoder().decode(TestObject.self, from: nilData) // key not found: "value"

Is this expected behaviour?
Maybe my understanding of implicitly unwrapped optionals is incomplete.
I thought passing in an implicitly unwrapped optional where a concrete instance is expected would implicitly unwrap it at runtime, and the decoder would be able to decode the object correctly.

Curious. What OS version are you testing on, and is this in a debug build or a release build?

FWIW, I can't reproduce this either on macOS Monterey 12.4 or in an iOS 15.5 simulator, so something might be up with the setup here. This is definitely not expected. Is this the fully minimal reproducing test case for you, or is this something you pulled out of a main app?

I'm on Monterey 12.3.1, testing on an iOS 15.4 simulator. This is indeed a minimal reproducing test case.

I realised this might have something to do with the different decode overloads.

func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable
func decode<T>(_ type: T.Type, from object: Any) throws -> T where T : Decodable

From what I understand, the use case for the latter is decoding [String: Any] dictionaries.
This is the overload that the compiler will choose when passing in an object of type Data! - which might explain the lack of a Unexpectedly found nil while implicitly unwrapping an Optional value being thrown.

Edit: My mistake. Looks like the overload actually came from the project, and is what's causing this behaviour.

func decode<T: Decodable>(_ type: T.Type , from object: Any) throws -> T {
    return try decode(type, from: try JSONSerialization.data(withJSONObject: userInfo))
}

Ah, yeah, that would absolutely do it. I had a hunch that this might be what's happening.

You're absolutely right that Data! will fall into this path, which in this case will cause it to get re-encoded into JSON, which is why the resulting data doesn't contain the value key that you expect.

It's also why passing in a nil Data! works just fine: because the Data! value gets boxed up in an Any box, it's not actually evaluated at the callsite, but passed directly into JSONSerialization in Objective-C, which is a lot more lenient with nil values. You'll get data back out (likely an empty JSON object string), which will then also be missing the key.

1 Like

When you pass an implicitly-unwrapped optional Data! into try JSONDecoder().decode, it's equivalent to passing in a normal optional Data? because Optional<T> conforms to Decodable when T conforms to Decodable. See The Swift Programming Language: Redirect.

// The Data instances are equivalent
XCTAssertEqual(implicitlyUnwrappedData, concreteData)

No, they're not equivalent. implicitlyUnwrappedData gets coerced from Data! to Data because concreteData is of type Data.

tl;dr

Don't use implicitly-unwrapped Optionals.

I think you may have misread the original post — this isn't about decoding a Data.self vs. Optional<Data>.self in a Decodable context, but about the from: parameter, which statically takes only Data. The issue here was a project-specific overload which took a different parameter for the from: parameter, which is an unrelated concern.