Skip to content

Commit 6d5c8fc

Browse files
committed
Conform DecodingError to CustomStringConvertible and return a tidier description for debugging. Also improve the default description of CodingKey.
1 parent 23a1817 commit 6d5c8fc

File tree

2 files changed

+229
-2
lines changed

2 files changed

+229
-2
lines changed

stdlib/public/core/Codable.swift

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,11 @@ public protocol CodingKey: Sendable,
8181
extension CodingKey {
8282
/// A textual representation of this key.
8383
public var description: String {
84-
let intValue = self.intValue?.description ?? "nil"
85-
return "\(type(of: self))(stringValue: \"\(stringValue)\", intValue: \(intValue))"
84+
if let intValue {
85+
"[\(intValue)]"
86+
} else {
87+
stringValue
88+
}
8689
}
8790

8891
/// A textual representation of this key, suitable for debugging.
@@ -3724,6 +3727,42 @@ public enum DecodingError: Error {
37243727
}
37253728
}
37263729

3730+
@available(SwiftStdlib 6.2, *)
3731+
extension DecodingError: CustomStringConvertible {
3732+
public var description: String {
3733+
let context: Context = switch self {
3734+
case .typeMismatch(_, let context):
3735+
context
3736+
case .valueNotFound(_, let context):
3737+
context
3738+
case .keyNotFound(_, let context):
3739+
context
3740+
case .dataCorrupted(let context):
3741+
context
3742+
}
3743+
var output = context.debugDescription
3744+
if let underlyingError = context.underlyingError {
3745+
output.append("\n")
3746+
output.append("Underlying error: \(underlyingError)")
3747+
}
3748+
if !context.codingPath.isEmpty {
3749+
output.append("\n")
3750+
output.append("Path: \(context.codingPath.errorPresentationDescription)")
3751+
}
3752+
3753+
return output
3754+
}
3755+
}
3756+
3757+
private extension [any CodingKey] {
3758+
/// Concatenates the elements of an array of coding keys and joins them with "/" separators to make them read like a path.
3759+
var errorPresentationDescription: String {
3760+
self
3761+
.map { $0.description }
3762+
.joined(separator: "/")
3763+
}
3764+
}
3765+
37273766
// The following extensions allow for easier error construction.
37283767

37293768
internal struct _GenericIndexKey: CodingKey, Sendable {

test/stdlib/CodableTests.swift

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1100,6 +1100,170 @@ class TestCodable : TestCodableSuper {
11001100
expectRoundTripEqualityThroughPlist(for: UUIDCodingWrapper(uuid), lineNumber: testLine)
11011101
}
11021102
}
1103+
1104+
// MARK: - DecodingError
1105+
func expectErrorDescription(
1106+
_ expectedErrorDescription: String,
1107+
fromDecodingError error: DecodingError,
1108+
lineNumber: UInt = #line
1109+
) {
1110+
expectEqual(String(describing: error), expectedErrorDescription, "Unexpectedly wrong error: \(error)", line: lineNumber)
1111+
}
1112+
1113+
func test_decodingError_typeMismatch_nilUnderlyingError() {
1114+
expectErrorDescription(
1115+
#"""
1116+
Expected to decode String but found number instead
1117+
Path: [0]/address/city/birds/[1]/name
1118+
"""#,
1119+
fromDecodingError: DecodingError.typeMismatch(
1120+
String.self,
1121+
DecodingError.Context(
1122+
codingPath: [0, "address", "city", "birds", 1, "name"] as [GenericCodingKey],
1123+
debugDescription: "Expected to decode String but found number instead"
1124+
)
1125+
)
1126+
)
1127+
}
1128+
1129+
func test_decodingError_typeMismatch_nonNilUnderlyingError() {
1130+
expectErrorDescription(
1131+
#"""
1132+
Expected to decode String but found number instead
1133+
Underlying error: GenericError(name: "some generic error goes here")
1134+
Path: [0]/address/[1]/street
1135+
"""#,
1136+
fromDecodingError: DecodingError.typeMismatch(
1137+
String.self,
1138+
DecodingError.Context(
1139+
codingPath: [0, "address", 1, "street"] as [GenericCodingKey],
1140+
debugDescription: "Expected to decode String but found number instead",
1141+
underlyingError: GenericError(name: "some generic error goes here")
1142+
)
1143+
)
1144+
)
1145+
}
1146+
1147+
func test_decodingError_valueNotFound_nilUnderlyingError() {
1148+
expectErrorDescription(
1149+
#"""
1150+
Cannot get value of type String -- found null value instead
1151+
Path: [0]/firstName
1152+
"""#,
1153+
fromDecodingError: DecodingError.valueNotFound(
1154+
String.self,
1155+
DecodingError.Context(
1156+
codingPath: [0, "firstName"] as [GenericCodingKey],
1157+
debugDescription: "Cannot get value of type String -- found null value instead"
1158+
)
1159+
)
1160+
)
1161+
}
1162+
1163+
func test_decodingError_valueNotFound_nonNilUnderlyingError() {
1164+
expectErrorDescription(
1165+
#"""
1166+
Cannot get value of type String -- found null value instead
1167+
Underlying error: GenericError(name: "these aren\'t the droids you\'re looking for")
1168+
Path: [0]/firstName
1169+
"""#,
1170+
fromDecodingError: DecodingError.valueNotFound(
1171+
String.self,
1172+
DecodingError.Context(
1173+
codingPath: [0, "firstName"] as [GenericCodingKey],
1174+
debugDescription: "Cannot get value of type String -- found null value instead",
1175+
underlyingError: GenericError(name: "these aren't the droids you're looking for")
1176+
)
1177+
)
1178+
)
1179+
}
1180+
1181+
func test_decodingError_keyNotFound_nilUnderlyingError() {
1182+
expectErrorDescription(
1183+
#"""
1184+
No value associated with key name ("name")
1185+
Path: [0]/address/city
1186+
"""#,
1187+
fromDecodingError: DecodingError.keyNotFound(
1188+
GenericCodingKey(stringValue: "name"),
1189+
DecodingError.Context(
1190+
codingPath: [0, "address", "city"] as [GenericCodingKey],
1191+
debugDescription: #"""
1192+
No value associated with key name ("name")
1193+
"""#
1194+
)
1195+
)
1196+
)
1197+
}
1198+
1199+
func test_decodingError_keyNotFound_nonNilUnderlyingError() {
1200+
expectErrorDescription(
1201+
#"""
1202+
No value associated with key name ("name")
1203+
Underlying error: GenericError(name: "hey, who turned out the lights?")
1204+
Path: [0]/address/city
1205+
"""#,
1206+
fromDecodingError: DecodingError.keyNotFound(
1207+
GenericCodingKey(stringValue: "name"),
1208+
DecodingError.Context(
1209+
codingPath: [0, "address", "city"] as [GenericCodingKey],
1210+
debugDescription: #"""
1211+
No value associated with key name ("name")
1212+
"""#,
1213+
underlyingError: GenericError(name: "hey, who turned out the lights?")
1214+
)
1215+
)
1216+
)
1217+
}
1218+
1219+
func test_decodingError_dataCorrupted_emptyCodingPath() {
1220+
expectErrorDescription(
1221+
#"""
1222+
The given data was not valid JSON
1223+
Underlying error: Error Domain=NSCocoaErrorDomain Code=3840 "Unexpected character 'a' around line 5, column 10." UserInfo={NSJSONSerializationErrorIndex=81, NSDebugDescription=Unexpected character 'a' around line 5, column 10.}
1224+
"""#,
1225+
fromDecodingError: DecodingError.dataCorrupted(
1226+
DecodingError.Context(
1227+
codingPath: [] as [GenericCodingKey], // sometimes empty when generated by JSONDecoder
1228+
debugDescription: "The given data was not valid JSON",
1229+
// Real-world example error from JSONDecoder, although it does not correspond to the given codingPath in this test
1230+
underlyingError: NSError(
1231+
domain: NSCocoaErrorDomain,
1232+
code: 3840,
1233+
userInfo: [
1234+
"NSJSONSerializationErrorIndex": 81,
1235+
"NSDebugDescription": "Unexpected character 'a' around line 5, column 10.",
1236+
]
1237+
)
1238+
)
1239+
)
1240+
)
1241+
}
1242+
1243+
func test_decodingError_dataCorrupted_nonEmptyCodingPath() {
1244+
expectErrorDescription(
1245+
#"""
1246+
The given data was not valid JSON
1247+
Underlying error: Error Domain=NSCocoaErrorDomain Code=3840 "Unexpected character 'a' around line 5, column 10." UserInfo={NSJSONSerializationErrorIndex=81, NSDebugDescription=Unexpected character 'a' around line 5, column 10.}
1248+
Path: first/second/[2]
1249+
"""#,
1250+
fromDecodingError: DecodingError.dataCorrupted(
1251+
DecodingError.Context(
1252+
codingPath: ["first", "second", 2] as [GenericCodingKey],
1253+
debugDescription: "The given data was not valid JSON",
1254+
// Real-world example error from JSONDecoder, although it does not correspond to the given codingPath in this test
1255+
underlyingError: NSError(
1256+
domain: NSCocoaErrorDomain,
1257+
code: 3840,
1258+
userInfo: [
1259+
"NSJSONSerializationErrorIndex": 81,
1260+
"NSDebugDescription": "Unexpected character 'a' around line 5, column 10.",
1261+
]
1262+
)
1263+
)
1264+
)
1265+
)
1266+
}
11031267
}
11041268

11051269
// MARK: - Helper Types
@@ -1118,6 +1282,18 @@ struct GenericCodingKey: CodingKey {
11181282
}
11191283
}
11201284

1285+
extension GenericCodingKey: ExpressibleByStringLiteral {
1286+
init(stringLiteral: String) {
1287+
self.init(stringValue: stringLiteral)
1288+
}
1289+
}
1290+
1291+
extension GenericCodingKey: ExpressibleByIntegerLiteral {
1292+
init(integerLiteral: Int) {
1293+
self.init(intValue: integerLiteral)
1294+
}
1295+
}
1296+
11211297
struct TopLevelWrapper<T> : Codable, Equatable where T : Codable, T : Equatable {
11221298
let value: T
11231299

@@ -1130,6 +1306,10 @@ struct TopLevelWrapper<T> : Codable, Equatable where T : Codable, T : Equatable
11301306
}
11311307
}
11321308

1309+
struct GenericError: Error {
1310+
let name: String
1311+
}
1312+
11331313
// MARK: - Tests
11341314

11351315
#if !FOUNDATION_XCTEST
@@ -1183,6 +1363,14 @@ var tests = [
11831363
"test_URL_Plist" : TestCodable.test_URL_Plist,
11841364
"test_UUID_JSON" : TestCodable.test_UUID_JSON,
11851365
"test_UUID_Plist" : TestCodable.test_UUID_Plist,
1366+
"test_decodingError_typeMismatch_nilUnderlyingError": TestCodable.test_decodingError_typeMismatch_nilUnderlyingError,
1367+
"test_decodingError_typeMismatch_nonNilUnderlyingError": TestCodable.test_decodingError_typeMismatch_nonNilUnderlyingError,
1368+
"test_decodingError_valueNotFound_nilUnderlyingError": TestCodable.test_decodingError_valueNotFound_nilUnderlyingError,
1369+
"test_decodingError_valueNotFound_nonNilUnderlyingError": TestCodable.test_decodingError_valueNotFound_nonNilUnderlyingError,
1370+
"test_decodingError_keyNotFound_nilUnderlyingError": TestCodable.test_decodingError_keyNotFound_nilUnderlyingError,
1371+
"test_decodingError_keyNotFound_nonNilUnderlyingError": TestCodable.test_decodingError_keyNotFound_nonNilUnderlyingError,
1372+
"test_decodingError_dataCorrupted_emptyCodingPath": TestCodable.test_decodingError_dataCorrupted_emptyCodingPath,
1373+
"test_decodingError_dataCorrupted_nonEmptyCodingPath": TestCodable.test_decodingError_dataCorrupted_nonEmptyCodingPath,
11861374
]
11871375

11881376
#if os(macOS)

0 commit comments

Comments
 (0)