: Decodable { let property: T static func de" /> : Decodable { let property: T static func de" /> : Decodable { let property: T static func de"/>

Swift decoding error types inconsistency with `Bool` type

60 Views Asked by At
import Foundation

let json = """
{
    "property": null
}
""".data(using: .utf8)!

struct Program<T: Decodable>: Decodable {
    let property: T
    
    static func decode() {
        do {
            try JSONDecoder().decode(Self.self, from: json)
        } catch {
            print("Error decoding \(T.self): \(error)\n")
        }
    }
}

Program<String>.decode()
Program<Int>.decode()
Program<[Double]>.decode()
Program<[String: Int]>.decode()
Program<Bool>.decode()

For every case, except for Bool, we get valueNotFound("Cannot get unkeyed decoding container -- found null value instead") error. Which is correct, according to the documentation.
Only for Bool, for some reason, we get typeMismatch("Expected to decode Bool but found null instead.") error.

2

There are 2 best solutions below

2
Sweeper On BEST ANSWER

On Linux (fiddle), [Double] produces a valueNotFound and everything else produces a typeMismatch. This is consistent with the source of swift-core-libs-foundation, where unkeyedContainer (which is called when decoding the array) throws valueNotFound (source):

@usableFromInline func unkeyedContainer() throws -> UnkeyedDecodingContainer {
    switch self.json {
    case .array(let array):
        ...
    case .null:
        throw DecodingError.valueNotFound([String: JSONValue].self, DecodingError.Context(
            codingPath: self.codingPath, 
            debugDescription: "Cannot get unkeyed decoding container -- found null value instead"
        ))
    default:
        ...
    }
}

whereas typeMismatch is thrown by the single value container created for decoding the "scalar" types (source).

func decode(_: Bool.Type) throws -> Bool {
    guard case .bool(let bool) = self.value else {
        throw self.impl.createTypeMismatchError(type: Bool.self, value: self.value)
    }

    return bool
}

Because of this, I'd suspect JSONDecoder on macOS is not purposefully implemented for Bool to throw typeMismatch and the others valueNotFound. It seems to use the JSONSerialization API to decode the JSON to an NSDictionary first. The behaviour of Bool vs the other types could be quirk of as a consequence of that.

I personally think the wording in the documentation for typeMismatch and valueNotFound are loose enough that it would be "correct" to throw either, when encountering a null value.

0
l'L'l On

When decoding JSON with Swift's JSONDecoder, if a non-optional type like String, Int, [Double], or [String: Int] encounters a null value, it throws a valueNotFound error because it can't decode null into a non-optional type.

However, for Bool, which can only be true or false, encountering null throws a typeMismatch error because null isn't considered a valid boolean value.

To handle this, you can make your property type optional if you expect null values in your JSON.

struct Program<T: Decodable>: Decodable {
    let property: T?
    
    static func decode() {
        do {
            try JSONDecoder().decode(Self.self, from: json)
        } catch {
            print("Error decoding \(T.self): \(error)\n")
        }
    }
}