Skip to content

Commit 2f00b83

Browse files
committed
Conform EncodingError and DecodingError to CustomDebugStringConvertible and return a tidy debugDescription to make errors easier to read in debug output.
1 parent 9f5c090 commit 2f00b83

File tree

2 files changed

+337
-0
lines changed

2 files changed

+337
-0
lines changed

stdlib/public/core/Codable.swift

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,26 @@ 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+
}
103+
}
104+
105+
private extension [any CodingKey] {
106+
/// Concatenates the elements of an array of coding keys and joins them with "/" separators to make them read like a path.
107+
var errorPresentationDescription: String {
108+
self
109+
.map { $0.errorPresentationDescription } // Can't use .map(\.errorPresentationDescription) due to https://github.com/swiftlang/swift/issues/80716
110+
.joined(separator: "/")
111+
}
92112
}
93113

94114
//===----------------------------------------------------------------------===//
@@ -3724,6 +3744,64 @@ public enum DecodingError: Error {
37243744
}
37253745
}
37263746

3747+
@available(SwiftStdlib 6.2, *)
3748+
extension EncodingError: CustomDebugStringConvertible {
3749+
public var debugDescription: String {
3750+
let (message, context) = switch self {
3751+
case .invalidValue(let value, let context):
3752+
(
3753+
"Invalid value: \(String(reflecting: value)) (\(type(of: value)))",
3754+
context
3755+
)
3756+
}
3757+
3758+
var output = """
3759+
\(message)
3760+
\(context.debugDescription)
3761+
"""
3762+
3763+
if let underlyingError = context.underlyingError {
3764+
output.append("\nUnderlying error: \(underlyingError)")
3765+
}
3766+
if !context.codingPath.isEmpty {
3767+
output.append("\nPath: \(context.codingPath.errorPresentationDescription)")
3768+
}
3769+
3770+
return output
3771+
}
3772+
}
3773+
3774+
@available(SwiftStdlib 6.2, *)
3775+
extension DecodingError: CustomDebugStringConvertible {
3776+
public var debugDescription: String {
3777+
let (message, context) = switch self {
3778+
case .typeMismatch(let expectedType, let context):
3779+
("Type mismatch: expected value of type \(expectedType).", context)
3780+
case .valueNotFound(let expectedType, let context):
3781+
("Expected value of type \(expectedType) but found null instead.", context)
3782+
case .keyNotFound(let expectedKey, let context):
3783+
("Key '\(expectedKey.errorPresentationDescription)' not found in keyed decoding container.", context)
3784+
case .dataCorrupted(let context):
3785+
("Data was corrupted.", context)
3786+
}
3787+
3788+
var output = message
3789+
3790+
if !context.debugDescription.isEmpty {
3791+
output.append("\nDebug description: \(context.debugDescription)")
3792+
}
3793+
3794+
if let underlyingError = context.underlyingError {
3795+
output.append("\nUnderlying error: \(underlyingError)")
3796+
}
3797+
if !context.codingPath.isEmpty {
3798+
output.append("\nPath: \(context.codingPath.errorPresentationDescription)")
3799+
}
3800+
3801+
return output
3802+
}
3803+
}
3804+
37273805
// The following extensions allow for easier error construction.
37283806

37293807
internal struct _GenericIndexKey: CodingKey, Sendable {

test/stdlib/CodableTests.swift

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1100,6 +1100,237 @@ 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+
Type mismatch: expected value of type String.
1117+
Debug description: This is where the debug description goes
1118+
Path: [0]/address/city/birds/[1]/name
1119+
"""#,
1120+
fromDecodingError: DecodingError.typeMismatch(
1121+
String.self,
1122+
DecodingError.Context(
1123+
codingPath: [0, "address", "city", "birds", 1, "name"] as [GenericCodingKey],
1124+
debugDescription: "This is where the debug description goes"
1125+
)
1126+
)
1127+
)
1128+
}
1129+
1130+
func test_decodingError_typeMismatch_nonNilUnderlyingError() {
1131+
expectErrorDescription(
1132+
#"""
1133+
Type mismatch: expected value of type String.
1134+
Debug description: Some debug description
1135+
Underlying error: GenericError(name: "some generic error goes here")
1136+
Path: [0]/address/[1]/street
1137+
"""#,
1138+
fromDecodingError: DecodingError.typeMismatch(
1139+
String.self,
1140+
DecodingError.Context(
1141+
codingPath: [0, "address", 1, "street"] as [GenericCodingKey],
1142+
debugDescription: "Some debug description",
1143+
underlyingError: GenericError(name: "some generic error goes here")
1144+
)
1145+
)
1146+
)
1147+
}
1148+
1149+
func test_decodingError_valueNotFound_nilUnderlyingError() {
1150+
expectErrorDescription(
1151+
#"""
1152+
Expected value of type String but found null instead.
1153+
Debug description: Description for debugging purposes
1154+
Path: [0]/firstName
1155+
"""#,
1156+
fromDecodingError: DecodingError.valueNotFound(
1157+
String.self,
1158+
DecodingError.Context(
1159+
codingPath: [0, "firstName"] as [GenericCodingKey],
1160+
debugDescription: "Description for debugging purposes"
1161+
)
1162+
)
1163+
)
1164+
}
1165+
1166+
func test_decodingError_valueNotFound_nonNilUnderlyingError() {
1167+
expectErrorDescription(
1168+
#"""
1169+
Expected value of type Int but found null instead.
1170+
Debug description: Here is the debug description for value-not-found
1171+
Underlying error: GenericError(name: "these aren\'t the droids you\'re looking for")
1172+
Path: [0]/population
1173+
"""#,
1174+
fromDecodingError: DecodingError.valueNotFound(
1175+
Int.self,
1176+
DecodingError.Context(
1177+
codingPath: [0, "population"] as [GenericCodingKey],
1178+
debugDescription: "Here is the debug description for value-not-found",
1179+
underlyingError: GenericError(name: "these aren't the droids you're looking for")
1180+
)
1181+
)
1182+
)
1183+
}
1184+
1185+
func test_decodingError_keyNotFound_nilUnderlyingError() {
1186+
expectErrorDescription(
1187+
#"""
1188+
Key 'name' not found in keyed decoding container.
1189+
Debug description: How would you describe your relationship with your debugger?
1190+
Path: [0]/address/city
1191+
"""#,
1192+
fromDecodingError: DecodingError.keyNotFound(
1193+
GenericCodingKey(stringValue: "name"),
1194+
DecodingError.Context(
1195+
codingPath: [0, "address", "city"] as [GenericCodingKey],
1196+
debugDescription: "How would you describe your relationship with your debugger?"
1197+
)
1198+
)
1199+
)
1200+
}
1201+
1202+
func test_decodingError_keyNotFound_nonNilUnderlyingError() {
1203+
expectErrorDescription(
1204+
#"""
1205+
Key 'name' not found in keyed decoding container.
1206+
Debug description: Just some info to help you out
1207+
Underlying error: GenericError(name: "hey, who turned out the lights?")
1208+
Path: [0]/address/city
1209+
"""#,
1210+
fromDecodingError: DecodingError.keyNotFound(
1211+
GenericCodingKey(stringValue: "name"),
1212+
DecodingError.Context(
1213+
codingPath: [0, "address", "city"] as [GenericCodingKey],
1214+
debugDescription: "Just some info to help you out",
1215+
underlyingError: GenericError(name: "hey, who turned out the lights?")
1216+
)
1217+
)
1218+
)
1219+
}
1220+
1221+
func test_decodingError_dataCorrupted_emptyCodingPath() {
1222+
expectErrorDescription(
1223+
#"""
1224+
Data was corrupted.
1225+
Debug description: The given data was not valid JSON
1226+
Underlying error: GenericError(name: "just some data corruption")
1227+
"""#,
1228+
fromDecodingError: DecodingError.dataCorrupted(
1229+
DecodingError.Context(
1230+
codingPath: [] as [GenericCodingKey], // sometimes empty when generated by JSONDecoder
1231+
debugDescription: "The given data was not valid JSON",
1232+
underlyingError: GenericError(name: "just some data corruption")
1233+
)
1234+
)
1235+
)
1236+
}
1237+
1238+
func test_decodingError_dataCorrupted_nonEmptyCodingPath() {
1239+
expectErrorDescription(
1240+
#"""
1241+
Data was corrupted.
1242+
Debug description: There was apparently some data corruption!
1243+
Underlying error: GenericError(name: "This data corruption is getting out of hand")
1244+
Path: first/second/[2]
1245+
"""#,
1246+
fromDecodingError: DecodingError.dataCorrupted(
1247+
DecodingError.Context(
1248+
codingPath: ["first", "second", 2] as [GenericCodingKey],
1249+
debugDescription: "There was apparently some data corruption!",
1250+
underlyingError: GenericError(name: "This data corruption is getting out of hand")
1251+
)
1252+
)
1253+
)
1254+
}
1255+
1256+
// MARK: - EncodingError
1257+
func expectErrorDescription(
1258+
_ expectedErrorDescription: String,
1259+
fromEncodingError error: EncodingError,
1260+
lineNumber: UInt = #line
1261+
) {
1262+
expectEqual(String(describing: error), expectedErrorDescription, "Unexpectedly wrong error: \(error)", line: lineNumber)
1263+
}
1264+
1265+
func test_encodingError_invalidValue_emptyCodingPath_nilUnderlyingError() {
1266+
expectErrorDescription(
1267+
#"""
1268+
Invalid value: 123 (Int)
1269+
You cannot do that!
1270+
"""#,
1271+
fromEncodingError: EncodingError.invalidValue(
1272+
123 as Int,
1273+
EncodingError.Context(
1274+
codingPath: [] as [GenericCodingKey],
1275+
debugDescription: "You cannot do that!"
1276+
)
1277+
)
1278+
)
1279+
}
1280+
1281+
func test_encodingError_invalidValue_nonEmptyCodingPath_nilUnderlyingError() {
1282+
expectErrorDescription(
1283+
#"""
1284+
Invalid value: 234 (Int)
1285+
You cannot do that!
1286+
Path: first/second/[2]
1287+
"""#,
1288+
fromEncodingError: EncodingError.invalidValue(
1289+
234 as Int,
1290+
EncodingError.Context(
1291+
codingPath: ["first", "second", 2] as [GenericCodingKey],
1292+
debugDescription: "You cannot do that!"
1293+
)
1294+
)
1295+
)
1296+
}
1297+
1298+
func test_encodingError_invalidValue_emptyCodingPath_nonNilUnderlyingError() {
1299+
expectErrorDescription(
1300+
#"""
1301+
Invalid value: 345 (Int)
1302+
You cannot do that!
1303+
Underlying error: GenericError(name: "You really cannot do that")
1304+
"""#,
1305+
fromEncodingError: EncodingError.invalidValue(
1306+
345 as Int,
1307+
EncodingError.Context(
1308+
codingPath: [] as [GenericCodingKey],
1309+
debugDescription: "You cannot do that!",
1310+
underlyingError: GenericError(name: "You really cannot do that")
1311+
)
1312+
)
1313+
)
1314+
}
1315+
1316+
func test_encodingError_invalidValue_nonEmptyCodingPath_nonNilUnderlyingError() {
1317+
expectErrorDescription(
1318+
#"""
1319+
Invalid value: 456 (Int)
1320+
You cannot do that!
1321+
Underlying error: GenericError(name: "You really cannot do that")
1322+
Path: first/second/[2]
1323+
"""#,
1324+
fromEncodingError: EncodingError.invalidValue(
1325+
456 as Int,
1326+
EncodingError.Context(
1327+
codingPath: ["first", "second", 2] as [GenericCodingKey],
1328+
debugDescription: "You cannot do that!",
1329+
underlyingError: GenericError(name: "You really cannot do that")
1330+
)
1331+
)
1332+
)
1333+
}
11031334
}
11041335

11051336
// MARK: - Helper Types
@@ -1118,6 +1349,18 @@ struct GenericCodingKey: CodingKey {
11181349
}
11191350
}
11201351

1352+
extension GenericCodingKey: ExpressibleByStringLiteral {
1353+
init(stringLiteral: String) {
1354+
self.init(stringValue: stringLiteral)
1355+
}
1356+
}
1357+
1358+
extension GenericCodingKey: ExpressibleByIntegerLiteral {
1359+
init(integerLiteral: Int) {
1360+
self.init(intValue: integerLiteral)
1361+
}
1362+
}
1363+
11211364
struct TopLevelWrapper<T> : Codable, Equatable where T : Codable, T : Equatable {
11221365
let value: T
11231366

@@ -1130,6 +1373,10 @@ struct TopLevelWrapper<T> : Codable, Equatable where T : Codable, T : Equatable
11301373
}
11311374
}
11321375

1376+
struct GenericError: Error {
1377+
let name: String
1378+
}
1379+
11331380
// MARK: - Tests
11341381

11351382
#if !FOUNDATION_XCTEST
@@ -1183,6 +1430,18 @@ var tests = [
11831430
"test_URL_Plist" : TestCodable.test_URL_Plist,
11841431
"test_UUID_JSON" : TestCodable.test_UUID_JSON,
11851432
"test_UUID_Plist" : TestCodable.test_UUID_Plist,
1433+
"test_decodingError_typeMismatch_nilUnderlyingError" : TestCodable.test_decodingError_typeMismatch_nilUnderlyingError,
1434+
"test_decodingError_typeMismatch_nonNilUnderlyingError" : TestCodable.test_decodingError_typeMismatch_nonNilUnderlyingError,
1435+
"test_decodingError_valueNotFound_nilUnderlyingError" : TestCodable.test_decodingError_valueNotFound_nilUnderlyingError,
1436+
"test_decodingError_valueNotFound_nonNilUnderlyingError" : TestCodable.test_decodingError_valueNotFound_nonNilUnderlyingError,
1437+
"test_decodingError_keyNotFound_nilUnderlyingError" : TestCodable.test_decodingError_keyNotFound_nilUnderlyingError,
1438+
"test_decodingError_keyNotFound_nonNilUnderlyingError" : TestCodable.test_decodingError_keyNotFound_nonNilUnderlyingError,
1439+
"test_decodingError_dataCorrupted_emptyCodingPath" : TestCodable.test_decodingError_dataCorrupted_emptyCodingPath,
1440+
"test_decodingError_dataCorrupted_nonEmptyCodingPath" : TestCodable.test_decodingError_dataCorrupted_nonEmptyCodingPath,
1441+
"test_encodingError_invalidValue_emptyCodingPath_nilUnderlyingError": TestCodable.test_encodingError_invalidValue_emptyCodingPath_nilUnderlyingError,
1442+
"test_encodingError_invalidValue_nonEmptyCodingPath_nilUnderlyingError": TestCodable.test_encodingError_invalidValue_nonEmptyCodingPath_nilUnderlyingError,
1443+
"test_encodingError_invalidValue_emptyCodingPath_nonNilUnderlyingError": TestCodable.test_encodingError_invalidValue_emptyCodingPath_nonNilUnderlyingError,
1444+
"test_encodingError_invalidValue_nonEmptyCodingPath_nonNilUnderlyingError": TestCodable.test_encodingError_invalidValue_nonEmptyCodingPath_nonNilUnderlyingError,
11861445
]
11871446

11881447
#if os(macOS)

0 commit comments

Comments
 (0)