Skip to content

Commit 9c4cec9

Browse files
authored
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.
1 parent d248649 commit 9c4cec9

File tree

2 files changed

+68
-17
lines changed

2 files changed

+68
-17
lines changed

Sources/Foundation/JSONDecoder.swift

+48-17
Original file line numberDiff line numberDiff line change
@@ -227,34 +227,46 @@ extension JSONDecoderImpl: Decoder {
227227
@usableFromInline func container<Key>(keyedBy _: Key.Type) throws ->
228228
KeyedDecodingContainer<Key> where Key: CodingKey
229229
{
230-
guard case .object(let dictionary) = self.json else {
230+
switch self.json {
231+
case .object(let dictionary):
232+
let container = KeyedContainer<Key>(
233+
impl: self,
234+
codingPath: codingPath,
235+
dictionary: dictionary
236+
)
237+
return KeyedDecodingContainer(container)
238+
case .null:
239+
throw DecodingError.valueNotFound([String: JSONValue].self, DecodingError.Context(
240+
codingPath: self.codingPath,
241+
debugDescription: "Cannot get keyed decoding container -- found null value instead"
242+
))
243+
default:
231244
throw DecodingError.typeMismatch([String: JSONValue].self, DecodingError.Context(
232245
codingPath: self.codingPath,
233246
debugDescription: "Expected to decode \([String: JSONValue].self) but found \(self.json.debugDataTypeDescription) instead."
234247
))
235248
}
236-
237-
let container = KeyedContainer<Key>(
238-
impl: self,
239-
codingPath: codingPath,
240-
dictionary: dictionary
241-
)
242-
return KeyedDecodingContainer(container)
243249
}
244250

245251
@usableFromInline func unkeyedContainer() throws -> UnkeyedDecodingContainer {
246-
guard case .array(let array) = self.json else {
252+
switch self.json {
253+
case .array(let array):
254+
return UnkeyedContainer(
255+
impl: self,
256+
codingPath: self.codingPath,
257+
array: array
258+
)
259+
case .null:
260+
throw DecodingError.valueNotFound([String: JSONValue].self, DecodingError.Context(
261+
codingPath: self.codingPath,
262+
debugDescription: "Cannot get unkeyed decoding container -- found null value instead"
263+
))
264+
default:
247265
throw DecodingError.typeMismatch([JSONValue].self, DecodingError.Context(
248266
codingPath: self.codingPath,
249267
debugDescription: "Expected to decode \([JSONValue].self) but found \(self.json.debugDataTypeDescription) instead."
250268
))
251269
}
252-
253-
return UnkeyedContainer(
254-
impl: self,
255-
codingPath: self.codingPath,
256-
array: array
257-
)
258270
}
259271

260272
@usableFromInline func singleValueContainer() throws -> SingleValueDecodingContainer {
@@ -750,11 +762,11 @@ extension JSONDecoderImpl {
750762
}
751763

752764
func superDecoder() throws -> Decoder {
753-
try decoderForKey(_JSONKey.super)
765+
return decoderForKeyNoThrow(_JSONKey.super)
754766
}
755767

756768
func superDecoder(forKey key: K) throws -> Decoder {
757-
try decoderForKey(key)
769+
return decoderForKeyNoThrow(key)
758770
}
759771

760772
private func decoderForKey<LocalKey: CodingKey>(_ key: LocalKey) throws -> JSONDecoderImpl {
@@ -770,6 +782,25 @@ extension JSONDecoderImpl {
770782
)
771783
}
772784

785+
private func decoderForKeyNoThrow<LocalKey: CodingKey>(_ key: LocalKey) -> JSONDecoderImpl {
786+
let value: JSONValue
787+
do {
788+
value = try getValue(forKey: key)
789+
} catch {
790+
// if there no value for this key then return a null value
791+
value = .null
792+
}
793+
var newPath = self.codingPath
794+
newPath.append(key)
795+
796+
return JSONDecoderImpl(
797+
userInfo: self.impl.userInfo,
798+
from: value,
799+
codingPath: newPath,
800+
options: self.impl.options
801+
)
802+
}
803+
773804
@inline(__always) private func getValue<LocalKey: CodingKey>(forKey key: LocalKey) throws -> JSONValue {
774805
guard let value = dictionary[key.stringValue] else {
775806
throw DecodingError.keyNotFound(key, .init(

Tests/Foundation/Tests/TestJSONEncoder.swift

+20
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,25 @@ class TestJSONEncoder : XCTestCase {
456456
}
457457
}
458458

459+
func test_notFoundSuperDecoder() {
460+
struct NotFoundSuperDecoderTestType: Decodable {
461+
init(from decoder: Decoder) throws {
462+
let container = try decoder.container(keyedBy: CodingKeys.self)
463+
_ = try container.superDecoder(forKey: .superDecoder)
464+
}
465+
466+
private enum CodingKeys: String, CodingKey {
467+
case superDecoder = "super"
468+
}
469+
}
470+
let decoder = JSONDecoder()
471+
do {
472+
let _ = try decoder.decode(NotFoundSuperDecoderTestType.self, from: Data(#"{}"#.utf8))
473+
} catch {
474+
XCTFail("Caught error during decoding empty super decoder: \(error)")
475+
}
476+
}
477+
459478
// MARK: - Test encoding and decoding of built-in Codable types
460479
func test_codingOfBool() {
461480
test_codingOf(value: Bool(true), toAndFrom: "true")
@@ -1542,6 +1561,7 @@ extension TestJSONEncoder {
15421561
("test_encodeDecodeNumericTypesBaseline", test_encodeDecodeNumericTypesBaseline),
15431562
("test_nestedContainerCodingPaths", test_nestedContainerCodingPaths),
15441563
("test_superEncoderCodingPaths", test_superEncoderCodingPaths),
1564+
("test_notFoundSuperDecoder", test_notFoundSuperDecoder),
15451565
("test_codingOfBool", test_codingOfBool),
15461566
("test_codingOfNil", test_codingOfNil),
15471567
("test_codingOfInt8", test_codingOfInt8),

0 commit comments

Comments
 (0)