Found the cause. It is a change in behaviour between iOS 16 and 17, but a rather subtle one that shortcuts in my own code happen to turn into the mess you see above.
The change in behaviour is, SingleValueDecodingContainer.decode(Int.self)
when decoding a JSON literal such as 18.18
used to throw a DecodingError.dataCorrupted
(saying "parsed JSON number <18.18> does not fit in Int"); it now throws a JSONError.numberIsNotRepresentableInSwift
.
Mostly that's fine, because mostly when you call decode(Int.self)
the kind of failure doesn't much matter to you. But I was using that call as a "probe" to see if I could decode an Int, and catching specific error cases that I knew about. Which didn't include this one, so my retry logic for when the probe fails didn't run. Entirely my bad, as the contract doesn't make any claims whatsoever about what errors may be thrown on failure.
The error in my code is interesting in light of the ongoing discussion of typed throws over on Evolution: I was treating an entirely untyped throw as if I had the safety guarantees of a typed throw.
Why would you want to do that?!
(You may ask why on earth I needed to do that. Story too long for this margin to contain, but I'm using it to capture arbitrary JSON blobs as a sub-part of a generally strongly-typed Codable model. Its a solution, definition not the only solution, and very likely not the best solution either.)