Skip to content

Commit 37d1255

Browse files
committed
Conform DecodingError to CustomDebugStringConvertible and return a tidy debugDescription to make decoding errors easier to understand.
1 parent 3dc24b8 commit 37d1255

File tree

2 files changed

+217
-0
lines changed

2 files changed

+217
-0
lines changed

stdlib/public/core/Codable.swift

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,17 @@ extension CodingKey {
8989
public var debugDescription: String {
9090
return description
9191
}
92+
93+
/// A simplified description: the int value, if present, in square brackets.
94+
/// Otherwise, the string value by itself. Used when concatenating coding keys
95+
/// to form a path when printing debug information.
96+
var errorPresentationDescription: String {
97+
if let intValue {
98+
"[\(intValue)]"
99+
} else {
100+
stringValue
101+
}
102+
}
92103
}
93104

94105
//===----------------------------------------------------------------------===//
@@ -3724,6 +3735,42 @@ public enum DecodingError: Error {
37243735
}
37253736
}
37263737

3738+
@available(SwiftStdlib 6.2, *)
3739+
extension DecodingError: CustomDebugStringConvertible {
3740+
public var debugDescription: String {
3741+
let context: Context = switch self {
3742+
case .typeMismatch(_, let context):
3743+
context
3744+
case .valueNotFound(_, let context):
3745+
context
3746+
case .keyNotFound(_, let context):
3747+
context
3748+
case .dataCorrupted(let context):
3749+
context
3750+
}
3751+
var output = context.debugDescription
3752+
if let underlyingError = context.underlyingError {
3753+
output.append("\n")
3754+
output.append("Underlying error: \(underlyingError)")
3755+
}
3756+
if !context.codingPath.isEmpty {
3757+
output.append("\n")
3758+
output.append("Path: \(context.codingPath.errorPresentationDescription)")
3759+
}
3760+
3761+
return output
3762+
}
3763+
}
3764+
3765+
private extension [any CodingKey] {
3766+
/// Concatenates the elements of an array of coding keys and joins them with "/" separators to make them read like a path.
3767+
var errorPresentationDescription: String {
3768+
self
3769+
.map { $0.errorPresentationDescription } // Can't use .map(\.errorPresentationDescription) due to https://github.com/swiftlang/swift/issues/80716
3770+
.joined(separator: "/")
3771+
}
3772+
}
3773+
37273774
// The following extensions allow for easier error construction.
37283775

37293776
internal struct _GenericIndexKey: CodingKey, Sendable {

test/stdlib/CodableTests.swift

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1100,6 +1100,152 @@ 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+
This is where the debug description goes
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: "This is where the debug description goes"
1124+
)
1125+
)
1126+
)
1127+
}
1128+
1129+
func test_decodingError_typeMismatch_nonNilUnderlyingError() {
1130+
expectErrorDescription(
1131+
#"""
1132+
Some debug description
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: "Some debug description",
1141+
underlyingError: GenericError(name: "some generic error goes here")
1142+
)
1143+
)
1144+
)
1145+
}
1146+
1147+
func test_decodingError_valueNotFound_nilUnderlyingError() {
1148+
expectErrorDescription(
1149+
#"""
1150+
Description for debugging purposes
1151+
Path: [0]/firstName
1152+
"""#,
1153+
fromDecodingError: DecodingError.valueNotFound(
1154+
String.self,
1155+
DecodingError.Context(
1156+
codingPath: [0, "firstName"] as [GenericCodingKey],
1157+
debugDescription: "Description for debugging purposes"
1158+
)
1159+
)
1160+
)
1161+
}
1162+
1163+
func test_decodingError_valueNotFound_nonNilUnderlyingError() {
1164+
expectErrorDescription(
1165+
#"""
1166+
Here is the debug description for value-not-found
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: "Here is the debug description for value-not-found",
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+
How would you describe your relationship with your debugger?
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: "How would you describe your relationship with your debugger?"
1192+
)
1193+
)
1194+
)
1195+
}
1196+
1197+
func test_decodingError_keyNotFound_nonNilUnderlyingError() {
1198+
expectErrorDescription(
1199+
#"""
1200+
No value associated with key name ("name")
1201+
Underlying error: GenericError(name: "hey, who turned out the lights?")
1202+
Path: [0]/address/city
1203+
"""#,
1204+
fromDecodingError: DecodingError.keyNotFound(
1205+
GenericCodingKey(stringValue: "name"),
1206+
DecodingError.Context(
1207+
codingPath: [0, "address", "city"] as [GenericCodingKey],
1208+
debugDescription: #"""
1209+
No value associated with key name ("name")
1210+
"""#,
1211+
underlyingError: GenericError(name: "hey, who turned out the lights?")
1212+
)
1213+
)
1214+
)
1215+
}
1216+
1217+
func test_decodingError_dataCorrupted_emptyCodingPath() {
1218+
expectErrorDescription(
1219+
#"""
1220+
The given data was not valid JSON
1221+
Underlying error: GenericError(name: "just some data corruption")
1222+
"""#,
1223+
fromDecodingError: DecodingError.dataCorrupted(
1224+
DecodingError.Context(
1225+
codingPath: [] as [GenericCodingKey], // sometimes empty when generated by JSONDecoder
1226+
debugDescription: "The given data was not valid JSON",
1227+
underlyingError: GenericError(name: "just some data corruption")
1228+
)
1229+
)
1230+
)
1231+
}
1232+
1233+
func test_decodingError_dataCorrupted_nonEmptyCodingPath() {
1234+
expectErrorDescription(
1235+
#"""
1236+
There was apparently some data corruption!
1237+
Underlying error: GenericError(name: "This data corruption is getting out of hand")
1238+
Path: first/second/[2]
1239+
"""#,
1240+
fromDecodingError: DecodingError.dataCorrupted(
1241+
DecodingError.Context(
1242+
codingPath: ["first", "second", 2] as [GenericCodingKey],
1243+
debugDescription: "There was apparently some data corruption!",
1244+
underlyingError: GenericError(name: "This data corruption is getting out of hand")
1245+
)
1246+
)
1247+
)
1248+
}
11031249
}
11041250

11051251
// MARK: - Helper Types
@@ -1118,6 +1264,18 @@ struct GenericCodingKey: CodingKey {
11181264
}
11191265
}
11201266

1267+
extension GenericCodingKey: ExpressibleByStringLiteral {
1268+
init(stringLiteral: String) {
1269+
self.init(stringValue: stringLiteral)
1270+
}
1271+
}
1272+
1273+
extension GenericCodingKey: ExpressibleByIntegerLiteral {
1274+
init(integerLiteral: Int) {
1275+
self.init(intValue: integerLiteral)
1276+
}
1277+
}
1278+
11211279
struct TopLevelWrapper<T> : Codable, Equatable where T : Codable, T : Equatable {
11221280
let value: T
11231281

@@ -1130,6 +1288,10 @@ struct TopLevelWrapper<T> : Codable, Equatable where T : Codable, T : Equatable
11301288
}
11311289
}
11321290

1291+
struct GenericError: Error {
1292+
let name: String
1293+
}
1294+
11331295
// MARK: - Tests
11341296

11351297
#if !FOUNDATION_XCTEST
@@ -1183,6 +1345,14 @@ var tests = [
11831345
"test_URL_Plist" : TestCodable.test_URL_Plist,
11841346
"test_UUID_JSON" : TestCodable.test_UUID_JSON,
11851347
"test_UUID_Plist" : TestCodable.test_UUID_Plist,
1348+
"test_decodingError_typeMismatch_nilUnderlyingError": TestCodable.test_decodingError_typeMismatch_nilUnderlyingError,
1349+
"test_decodingError_typeMismatch_nonNilUnderlyingError": TestCodable.test_decodingError_typeMismatch_nonNilUnderlyingError,
1350+
"test_decodingError_valueNotFound_nilUnderlyingError": TestCodable.test_decodingError_valueNotFound_nilUnderlyingError,
1351+
"test_decodingError_valueNotFound_nonNilUnderlyingError": TestCodable.test_decodingError_valueNotFound_nonNilUnderlyingError,
1352+
"test_decodingError_keyNotFound_nilUnderlyingError": TestCodable.test_decodingError_keyNotFound_nilUnderlyingError,
1353+
"test_decodingError_keyNotFound_nonNilUnderlyingError": TestCodable.test_decodingError_keyNotFound_nonNilUnderlyingError,
1354+
"test_decodingError_dataCorrupted_emptyCodingPath": TestCodable.test_decodingError_dataCorrupted_emptyCodingPath,
1355+
"test_decodingError_dataCorrupted_nonEmptyCodingPath": TestCodable.test_decodingError_dataCorrupted_nonEmptyCodingPath,
11861356
]
11871357

11881358
#if os(macOS)

0 commit comments

Comments
 (0)