Skip to content

Conversation

@ZevEisenberg
Copy link
Contributor

@ZevEisenberg ZevEisenberg commented Apr 21, 2025

Now accepted as SE-0489, but we still need to refine the implementation.

To Do

  • confirm which version of Swift to use for the availability annotations. Probably 6.3 at time of writing.

Context

Re: Swift forum post, where a discussion about future serialization tools in Swift prompted Kevin Perry to suggest that some proposed changes could actually be made in today's stdlib.

Summary of Changes

Conforms EncodingError and DecodingError to CustomDebugStringConvertible and adds a more-readable debugDescription.

Future Directions

This is a pared-down version of some experiments I did in UsefulDecode. The changes in this PR are the best I could do without changing the public interface of DecodingError and EncodingError, and without modifying the way the JSON/PropertyList Encoder/Decoder in Foundation generate their errors' debug descriptions.

In the above-linked UsefulDecode repo, when JSON decoding fails, I go back and re-decode the JSON using JSONSerialization in order to provide more context about what failed, and why. I didn't attempt to make such a change here, but I'd like to discuss what may be possible.

Examples

To illustrate the effect of the changes in this PR, I removed my changes to stdlib/public/core/Codable.swift and ran my new test cases again. Here are the resulting diffs.

test_encodingError_invalidValue_nonEmptyCodingPath_nilUnderlyingError

Before

invalidValue(234, Swift.EncodingError.Context(codingPath: [GenericCodingKey(stringValue: "first", intValue: nil), GenericCodingKey(stringValue: "second", intValue: nil), GenericCodingKey(stringValue: "2", intValue: 2)], debugDescription: "You cannot do that!", underlyingError: nil))

After

EncodingError.invalidValue: 234 (Int). Path: first.second[2]. Debug description: You cannot do that!

test_decodingError_valueNotFound_nilUnderlyingError

Before

valueNotFound(Swift.String, Swift.DecodingError.Context(codingPath: [GenericCodingKey(stringValue: "0", intValue: 0), GenericCodingKey(stringValue: "firstName", intValue: nil)], debugDescription: "Description for debugging purposes", underlyingError: nil))

After

DecodingError.valueNotFound: Expected value of type String but found null instead. Path: [0].firstName. Debug description: Description for debugging purposes

test_decodingError_keyNotFound_nonNilUnderlyingError

Before

keyNotFound(GenericCodingKey(stringValue: "name", intValue: nil), Swift.DecodingError.Context(codingPath: [GenericCodingKey(stringValue: "0", intValue: 0), GenericCodingKey(stringValue: "address", intValue: nil), GenericCodingKey(stringValue: "city", intValue: nil)], debugDescription: "Just some info to help you out", underlyingError: Optional(main.GenericError(name: "hey, who turned out the lights?"))))

After

DecodingError.keyNotFound: Key \'name\' not found in keyed decoding container. Path: [0].address.city. Debug description: Just some info to help you out. Underlying error: GenericError(name: "hey, who turned out the lights?")

test_decodingError_typeMismatch_nilUnderlyingError

Before

typeMismatch(Swift.String, Swift.DecodingError.Context(codingPath: [GenericCodingKey(stringValue: "0", intValue: 0), GenericCodingKey(stringValue: "address", intValue: nil), GenericCodingKey(stringValue: "city", intValue: nil), GenericCodingKey(stringValue: "birds", intValue: nil), GenericCodingKey(stringValue: "1", intValue: 1), GenericCodingKey(stringValue: "name", intValue: nil)], debugDescription: "This is where the debug description goes", underlyingError: nil))

After

DecodingError.typeMismatch: expected value of type String. Path: [0].address.city.birds[1].name. Debug description: This is where the debug description goes

test_decodingError_dataCorrupted_nonEmptyCodingPath

Before

dataCorrupted(Swift.DecodingError.Context(codingPath: [GenericCodingKey(stringValue: "first", intValue: nil), GenericCodingKey(stringValue: "second", intValue: nil), GenericCodingKey(stringValue: "2", intValue: 2)], debugDescription: "There was apparently some data corruption!", underlyingError: Optional(main.GenericError(name: "This data corruption is getting out of hand"))))

After

DecodingError.dataCorrupted: Data was corrupted. Path: first.second[2]. Debug description: There was apparently some data corruption!. Underlying error: GenericError(name: "This data corruption is getting out of hand")

test_decodingError_valueNotFound_nonNilUnderlyingError

Before

valueNotFound(Swift.Int, Swift.DecodingError.Context(codingPath: [GenericCodingKey(stringValue: "0", intValue: 0), GenericCodingKey(stringValue: "population", intValue: nil)], debugDescription: "Here is the debug description for value-not-found", underlyingError: Optional(main.GenericError(name: "these aren\\\'t the droids you\\\'re looking for"))))

After

DecodingError.valueNotFound: Expected value of type Int but found null instead. Path: [0].population. Debug description: Here is the debug description for value-not-found. Underlying error: GenericError(name: "these aren\\\'t the droids you\\\'re looking for")

test_encodingError_invalidValue_emptyCodingPath_nonNilUnderlyingError

Before

invalidValue(345, Swift.EncodingError.Context(codingPath: [], debugDescription: "You cannot do that!", underlyingError: Optional(main.GenericError(name: "You really cannot do that"))))

After

EncodingError.invalidValue: 345 (Int). Debug description: You cannot do that!. Underlying error: GenericError(name: "You really cannot do that")

test_decodingError_typeMismatch_nonNilUnderlyingError

Before

typeMismatch(Swift.String, Swift.DecodingError.Context(codingPath: [GenericCodingKey(stringValue: "0", intValue: 0), GenericCodingKey(stringValue: "address", intValue: nil), GenericCodingKey(stringValue: "1", intValue: 1), GenericCodingKey(stringValue: "street", intValue: nil)], debugDescription: "Some debug description", underlyingError: Optional(main.GenericError(name: "some generic error goes here"))))

After

DecodingError.typeMismatch: expected value of type String. Path: [0].address[1].street. Debug description: Some debug description. Underlying error: GenericError(name: "some generic error goes here")

test_encodingError_invalidValue_emptyCodingPath_nilUnderlyingError

Before

invalidValue(123, Swift.EncodingError.Context(codingPath: [], debugDescription: "You cannot do that!", underlyingError: nil))

After

EncodingError.invalidValue: 123 (Int). Debug description: You cannot do that!

test_decodingError_keyNotFound_nilUnderlyingError

Before

keyNotFound(GenericCodingKey(stringValue: "name", intValue: nil), Swift.DecodingError.Context(codingPath: [GenericCodingKey(stringValue: "0", intValue: 0), GenericCodingKey(stringValue: "address", intValue: nil), GenericCodingKey(stringValue: "city", intValue: nil)], debugDescription: "How would you describe your relationship with your debugger?", underlyingError: nil))

After

DecodingError.keyNotFound: Key \'name\' not found in keyed decoding container. Path: [0]address.city. Debug description: How would you describe your relationship with your debugger?

test_decodingError_dataCorrupted_emptyCodingPath

Before

dataCorrupted(Swift.DecodingError.Context(codingPath: [], debugDescription: "The given data was not valid JSON", underlyingError: Optional(main.GenericError(name: "just some data corruption"))))

After

DecodingError.dataCorrupted: Data was corrupted. Debug description: The given data was not valid JSON. Underlying error: GenericError(name: "just some data corruption")

test_encodingError_invalidValue_nonEmptyCodingPath_nonNilUnderlyingError

Before

invalidValue(456, Swift.EncodingError.Context(codingPath: [GenericCodingKey(stringValue: "first", intValue: nil), GenericCodingKey(stringValue: "second", intValue: nil), GenericCodingKey(stringValue: "2", intValue: 2)], debugDescription: "You cannot do that!", underlyingError: Optional(main.GenericError(name: "You really cannot do that"))))

After

EncodingError.invalidValue: 456 (Int). Path: first.second[2]. Debug description: You cannot do that!. Underlying error: GenericError(name: "You really cannot do that")

@ZevEisenberg ZevEisenberg requested a review from a team as a code owner April 21, 2025 03:44
@ZevEisenberg ZevEisenberg force-pushed the main branch 2 times, most recently from 4cd346f to 1ae5683 Compare April 21, 2025 15:53
@stephentyrone
Copy link
Contributor

CC @kperryua

@ZevEisenberg ZevEisenberg changed the title Better description for DecodingError Better description for DecodingError and CodingKey Apr 21, 2025
Copy link
Contributor

@kperryua kperryua left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for this awesome contribution. The coding path description look so much more readable than present. A fair number of comments and suggestions, but hopefully easily resolved.

With regards to your suggestions about improving the error types in general: feel free to discuss this more in depth on the existing serialization thread, or a new one in the forums. DecodingError and EncodingError have to be discarded anyway in the new designs, because of their reliance on existentials, so we'll have a clean slate with which to make any fundamental improvements.

} else {
stringValue
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like a lone CodingKey ought to have a more complete description than just "[0]" or "keyName". Could this instead be a separate property (probably named similarly to the [any CodingKey] property) that gets called specifically in this context?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps, but an issue is that the description is also called by JSONDecoder in a few cases when providing debug descriptions for decoding error contexts, so it may clobber those too. Unless a) we're fine with that, or b) I'm misunderstanding what you're asking?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not a great idea for any external entity (JSONDecoder here) to expect a certain format out of another value's description, even for the purposes of string interpolation unless it's explicitly defined, like one can expect from Int or Double or whatever.

If JSONDecoder wants a certain format when printing CodingKeys in its descriptions, then it should do the work to generate that format itself. At present, that might involve some duplication of code between CodingKey and JSONDecoder / PropertyListDecoder / etc. unless/until that format can be wrapped in some API that does explicitly define the string format.

So, I still think CodingKey's description should remain untouched, and we perhaps follow up with a swift-foundation PR to improve the description construction for the cases you're talking about.

Copy link
Contributor Author

@ZevEisenberg ZevEisenberg Apr 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can make that change if you'd like: removing the changes to the description, and exposing the more concise version via a different variable. Shall I?

Also, just to make sure I understand: is this specifically because you don't want to change the existing implementation in case someone is relying on it? Or do you prefer the implementation of description regardless of concerns around breaking compatibility? Reading comprehension - your intent is clearer than I realized.

@pepicrft
Copy link

This would be such a great addition 👏🏼. We console-output errors in a CLI assuming the readability of the output, and users end up getting something that's impossible to debug.

@robinkunde
Copy link
Contributor

I love it! What do you think about using JSONPath for the paths to the troublesome values? It can speed up looking up the actual value, especially in larger documents.

@ZevEisenberg
Copy link
Contributor Author

I love it! What do you think about using JSONPath for the paths to the troublesome values? It can speed up looking up the actual value, especially in larger documents.

I'm not familiar with JSONPath. Can you say more? Also, the changes I'm making here in the stdlib are agnostic to the kind of decoder being used. I'm wondering if this suggestion would find a better home in either a PR to swift-foundation's JSONDecoder, or else as part of the nascent successor to Codable being discussed here.

@robinkunde
Copy link
Contributor

I'm not familiar with JSONPath. Can you say more? Also, the changes I'm making here in the stdlib are agnostic to the kind of decoder being used. I'm wondering if this suggestion would find a better home in either a PR to swift-foundation's JSONDecoder, or else as part of the nascent successor to Codable being discussed here.

JSONPath is a query expression syntax for JSON, like XPath for XML or perhaps CSS selectors. The key paths your PR constructs for typeMismatch/etc are already quite similar to it.
For example: [0]/address/city/birds/[1]/name would be $[0].address.city.birds[1].name when using JSONPath.

However, after reading over the diff, I realize there's nothing in DecodingError.Context that would let us special case this representation for JSON decoding specifically, which is too bad.

@ZevEisenberg
Copy link
Contributor Author

ZevEisenberg commented Apr 26, 2025

@kperryua I've addressed all feedback, and update the description to give an easy view of the new before/after test cases. I left a custom description of CodingKey because of the issue discussed above, where JSONDecoder is using it to format its error messages. I'm not opposed to changing it; I just wanted to discuss it more here. I also raised a question about empty string keys.

I tried to keep the commits atomic, but I figure I'll squash them all down once we're ready to merge.

@parkera
Copy link
Contributor

parkera commented Apr 27, 2025

@swift-ci test

@ZevEisenberg
Copy link
Contributor Author

This is my first time trying to read Swift CI failure messages. I'm also on my phone at the moment and Safari is struggling to render the CI failure page. I think I'm seeing something about a mangled name changing? Is that because of the added conformances? Do we need to do some kind of retroactive conformance or availability annotation or something?

@kperryua
Copy link
Contributor

--- /Users/ec2-user/jenkins/workspace/swift-PR-macos/branch-main/build/buildbot_incremental/swift-macosx-x86_64/test-macosx-x86_64/api-digester/Output/stability-stdlib-abi-with-asserts.test.tmp.tmp/stability-stdlib-abi.swift.expected.sorted	2025-04-27 06:56:06
+++ /Users/ec2-user/jenkins/workspace/swift-PR-macos/branch-main/build/buildbot_incremental/swift-macosx-x86_64/test-macosx-x86_64/api-digester/Output/stability-stdlib-abi-with-asserts.test.tmp.tmp/changes.txt.tmp	2025-04-27 06:56:06
@@ -187,6 +187,7 @@
 Constructor UnsafeRawPointer.init(_:) is now with @_preInverseGenerics
 Constructor _SmallString.init(taggedCocoa:) has mangled name changing from 'Swift._SmallString.init(taggedCocoa: Swift.AnyObject) -> Swift._SmallString' to 'Swift._SmallString.init(taggedCocoa: Swift.AnyObject) -> Swift.Optional<Swift._SmallString>'
 Constructor _SmallString.init(taggedCocoa:) has return type change from Swift._SmallString to Swift._SmallString?
+Enum DecodingError has added a conformance to an existing protocol CustomStringConvertible
 Enum MemoryLayout has generic signature change from <T> to <T where T : ~Copyable, T : ~Escapable>
 Enum Never has added a conformance to an existing protocol Decodable
 Enum Never has added a conformance to an existing protocol Encodable
@@ -670,6 +671,7 @@
 Subscript UnsafeMutableBufferPointer.subscript(_unchecked:) has been removed
 Subscript UnsafeMutablePointer.subscript(_:) has been removed
 Subscript UnsafePointer.subscript(_:) has been removed
+Var DecodingError.description is a new API without '@available'

Availability is required on the new conformance at a minimum. There might be some other files to adjust that acknowledge the addition to ABI. I'll try find to out what those are after we rebuild with availability and see what the tests say.

@ZevEisenberg
Copy link
Contributor Author

What should I do for availability on the conformances? Is it gated to a Swift version (like 6.2 or something)? I'd appreciate some guidance here, and then I can make the necessary changes, or feel free to make them yourself if that's easier.

@kperryua
Copy link
Contributor

@available(SwiftStdlib 6.2, *) is appropriate for current main I believe.

Copy link
Contributor

@kperryua kperryua left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also: seems like similar improvements could be made to EncodingError. Happy to treat that as a separate PR though.

@stephentyrone
Copy link
Contributor

@available(SwiftStdlib 6.2, *) is appropriate for current main I believe.

We've already branched, so if the intention is for this to go into 6.2 it will need a cherry-pick for the release/6.2 branch after landing on main. Otherwise, it would get 6.3 availability (which I think someone still has to add)

@stephentyrone
Copy link
Contributor

(ah, here's the PR for 6.3: #80870)

@ZevEisenberg
Copy link
Contributor Author

I'm happy to target whichever release seems appropriate. This has been 4+ years in the making; there's no particular urgency to get it into 6.2. That said, if it's not too painful to get it in, it would be nice to ship it sooner rather than later!

And as for EncodingError: I've been wondering if someone would bring it up! Time allowing, I'd love to make another PR.

@ZevEisenberg
Copy link
Contributor Author

ZevEisenberg commented Apr 29, 2025

I rebased on main, squashed commits, and added the availability annotation for 6.2 as requested. But that may not be needed, because I'm going to tweak the implementation per #80941 (comment).

@ZevEisenberg ZevEisenberg force-pushed the main branch 2 times, most recently from 67f6878 to 6d5c8fc Compare May 1, 2025 13:41
@ZevEisenberg
Copy link
Contributor Author

Oh, also, I don't know how important PR tags are, but I just noticed that this still has "swift evolution proposal needed" on it, and I can't change it.

@rintaro
Copy link
Member

rintaro commented Oct 21, 2025

@swift-ci Please test

@rintaro rintaro added swift evolution approved Flag → feature: A feature that was approved through the Swift evolution process and removed swift evolution proposal needed Flag → feature: A feature that warrants a Swift evolution proposal labels Oct 21, 2025
@ZevEisenberg
Copy link
Contributor Author

I'll have to look into it later, but these failed on macOS in this build.

17:29:35    Swift(iphonesimulator-x86_64) :: CAS/legacy_layout_remap.swift
17:29:35    Swift(iphonesimulator-x86_64) :: Interop/Cxx/stdlib/use-std-span.swift

@ZevEisenberg
Copy link
Contributor Author

For the above tests, I installed the latest main snapshot with swiftly and I rebased my branch on main. Then I built the compiler using:

./utils/build-script --release --skip-watchos --skip-tvos --skip-test-watchos --skip-test-tvos --skip-test-swiftpm true --skip-test-swift-driver true --skip-test-swiftsyntax true --skip-test-indexstore-db true --skip-test-sourcekit-lsp true --foundation false --skip-build-foundation --stdlib-deployment-targets macosx-arm64

When I run the first test like this:

./utils/run-test --lit ../llvm-project/llvm/utils/lit/lit.py --build-dir ../build/Ninja-ReleaseAssert --build=true ./test/CAS/legacy_layout_remap.swift

I get:

Total Discovered Tests: 1
  Unsupported: 1 (100.00%)

When I run the second test, using:

utils/run-test --lit ../llvm-project/llvm/utils/lit/lit.py --build-dir ../build/Ninja-ReleaseAssert --build=true ./test/Interop/Cxx/stdlib/use-std-span.swift

I get:

Total Discovered Tests: 1
  Passed: 1 (100.00%)

I've rebased and force-pushed my branch in case it's just fixed up updating to main, but otherwise, does anyone know how I might go about reproducing these failures so I can fix them locally?

@ZevEisenberg
Copy link
Contributor Author

ZevEisenberg commented Oct 22, 2025

(But, if anyone thinks a rebase alone is likely to fix it, could you please try a macOS or full build to check?)

@jamieQ
Copy link
Contributor

jamieQ commented Oct 22, 2025

@swift-ci please test

@jamieQ
Copy link
Contributor

jamieQ commented Oct 23, 2025

@swift-ci Please clean smoke test macOS platform

@ZevEisenberg
Copy link
Contributor Author

Thanks for the build help, @jamieQ! I'm wondering if this needs anything else to be ready to merge?

@ZevEisenberg
Copy link
Contributor Author

bump 🙏🏻

@ZevEisenberg
Copy link
Contributor Author

I had to rebase to address a conflict with a recent addition to the ABI tests. Could I please have a rebuild of all the required things? And @lorentey is there anything else you'd like to see before merging?

@kperryua
Copy link
Contributor

kperryua commented Nov 3, 2025

@swift-ci please test

@ZevEisenberg
Copy link
Contributor Author

Windows failed, but I didn't see anything that looked related to this PR, and I'm not currently equipped to test Windows locally. Anyone know what's up?

…bugStringConvertible and return a tidy debugDescription to make errors easier to read in debug output.
…s, backslashes, or backticks, and backslash-escape backslashes or backticks therein.
# Conflicts:
#	test/abi/macOS/arm64/stdlib.swift
…xplicitly note that the returned format is not stable.

This tends to be generally true for most `debugDescription` properties, but explicitly calling it out should help preventing people from trying to parse the result in this case.

Borrowed from swiftlang#75433 and SE-0445.
# Conflicts:
#	test/api-digester/stability-stdlib-abi-without-asserts.test
@ZevEisenberg
Copy link
Contributor Author

ZevEisenberg commented Nov 4, 2025

Rebased to resolve conflict in test/api-digester/stability-stdlib-abi-without-asserts.test. Could I have another build please? (I requested member access to be able to start my own builds.)

@Catfish-Man
Copy link
Contributor

@swift-ci please test

@ZevEisenberg
Copy link
Contributor Author

@lorentey @kperryua do you know whether the Windows build is required to merge here? it looks like it failed due to some kind of timeout. Please let me know if I should rebase or run it again, or if there's anything else holding up this PR.

@Catfish-Man
Copy link
Contributor

@swift-ci please test Windows platform

@ZevEisenberg
Copy link
Contributor Author

All builds are now passing 👀🙏🏻🤞🏻

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

swift evolution approved Flag → feature: A feature that was approved through the Swift evolution process

Projects

None yet

Development

Successfully merging this pull request may close these issues.