From 16f675b6b86936ad2c25927ea3aa9f5bb0943296 Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Thu, 8 Sep 2022 19:34:10 +0100 Subject: [PATCH] Fix `JSONDecoder` superDecoder darwin/linux discrepancy (#3167) * Don't throw DecodingError if superDecoder doesn't exist * Add decoderForKeyNoThrow * Improve JSONDecoder error handling When trying to created a keyed container or unkeyed containter from a null value throw a `DecodingError.valueNotFound` error instead of a `typeMismatch` error. This is more inline with Darwin. --- Sources/Foundation/JSONDecoder.swift | 65 +++++++++++++++----- Tests/Foundation/Tests/TestJSONEncoder.swift | 20 ++++++ 2 files changed, 68 insertions(+), 17 deletions(-) diff --git a/Sources/Foundation/JSONDecoder.swift b/Sources/Foundation/JSONDecoder.swift index a2d31776e7..faa3431631 100644 --- a/Sources/Foundation/JSONDecoder.swift +++ b/Sources/Foundation/JSONDecoder.swift @@ -227,34 +227,46 @@ extension JSONDecoderImpl: Decoder { @usableFromInline func container(keyedBy _: Key.Type) throws -> KeyedDecodingContainer where Key: CodingKey { - guard case .object(let dictionary) = self.json else { + switch self.json { + case .object(let dictionary): + let container = KeyedContainer( + impl: self, + codingPath: codingPath, + dictionary: dictionary + ) + return KeyedDecodingContainer(container) + case .null: + throw DecodingError.valueNotFound([String: JSONValue].self, DecodingError.Context( + codingPath: self.codingPath, + debugDescription: "Cannot get keyed decoding container -- found null value instead" + )) + default: throw DecodingError.typeMismatch([String: JSONValue].self, DecodingError.Context( codingPath: self.codingPath, debugDescription: "Expected to decode \([String: JSONValue].self) but found \(self.json.debugDataTypeDescription) instead." )) } - - let container = KeyedContainer( - impl: self, - codingPath: codingPath, - dictionary: dictionary - ) - return KeyedDecodingContainer(container) } @usableFromInline func unkeyedContainer() throws -> UnkeyedDecodingContainer { - guard case .array(let array) = self.json else { + switch self.json { + case .array(let array): + return UnkeyedContainer( + impl: self, + codingPath: self.codingPath, + array: 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: throw DecodingError.typeMismatch([JSONValue].self, DecodingError.Context( codingPath: self.codingPath, debugDescription: "Expected to decode \([JSONValue].self) but found \(self.json.debugDataTypeDescription) instead." )) } - - return UnkeyedContainer( - impl: self, - codingPath: self.codingPath, - array: array - ) } @usableFromInline func singleValueContainer() throws -> SingleValueDecodingContainer { @@ -750,11 +762,11 @@ extension JSONDecoderImpl { } func superDecoder() throws -> Decoder { - try decoderForKey(_JSONKey.super) + return decoderForKeyNoThrow(_JSONKey.super) } func superDecoder(forKey key: K) throws -> Decoder { - try decoderForKey(key) + return decoderForKeyNoThrow(key) } private func decoderForKey(_ key: LocalKey) throws -> JSONDecoderImpl { @@ -770,6 +782,25 @@ extension JSONDecoderImpl { ) } + private func decoderForKeyNoThrow(_ key: LocalKey) -> JSONDecoderImpl { + let value: JSONValue + do { + value = try getValue(forKey: key) + } catch { + // if there no value for this key then return a null value + value = .null + } + var newPath = self.codingPath + newPath.append(key) + + return JSONDecoderImpl( + userInfo: self.impl.userInfo, + from: value, + codingPath: newPath, + options: self.impl.options + ) + } + @inline(__always) private func getValue(forKey key: LocalKey) throws -> JSONValue { guard let value = dictionary[key.stringValue] else { throw DecodingError.keyNotFound(key, .init( diff --git a/Tests/Foundation/Tests/TestJSONEncoder.swift b/Tests/Foundation/Tests/TestJSONEncoder.swift index 919f04bbcb..d01eb24412 100644 --- a/Tests/Foundation/Tests/TestJSONEncoder.swift +++ b/Tests/Foundation/Tests/TestJSONEncoder.swift @@ -456,6 +456,25 @@ class TestJSONEncoder : XCTestCase { } } + func test_notFoundSuperDecoder() { + struct NotFoundSuperDecoderTestType: Decodable { + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + _ = try container.superDecoder(forKey: .superDecoder) + } + + private enum CodingKeys: String, CodingKey { + case superDecoder = "super" + } + } + let decoder = JSONDecoder() + do { + let _ = try decoder.decode(NotFoundSuperDecoderTestType.self, from: Data(#"{}"#.utf8)) + } catch { + XCTFail("Caught error during decoding empty super decoder: \(error)") + } + } + // MARK: - Test encoding and decoding of built-in Codable types func test_codingOfBool() { test_codingOf(value: Bool(true), toAndFrom: "true") @@ -1542,6 +1561,7 @@ extension TestJSONEncoder { ("test_encodeDecodeNumericTypesBaseline", test_encodeDecodeNumericTypesBaseline), ("test_nestedContainerCodingPaths", test_nestedContainerCodingPaths), ("test_superEncoderCodingPaths", test_superEncoderCodingPaths), + ("test_notFoundSuperDecoder", test_notFoundSuperDecoder), ("test_codingOfBool", test_codingOfBool), ("test_codingOfNil", test_codingOfNil), ("test_codingOfInt8", test_codingOfInt8),