keyDecodingStrategy and keys with custom raw values

I'd like to highlight to the community one counterintuitive, in my opinion, behaviour of JSONDecoder used with key decoding strategy. Imagine I have a type of a person defined like this:

struct Person: Codable {
    let firstName: String
    let surname: String
}

My API follows a snake-case notation for key so I'm going to use convertFromSnakeCase key decoding strategy. But for whatever reason, let's say historical, surname is returned by the API by last_name key. So I go and define custom coding keys:

struct Person: Codable {
    let firstName: String
    let surname: String

    enum CodingKeys: String, CodingKey {
        case firstName
        case surname = "last_name"
    }

I only define custom key hoping that key decoding strategy will figure out other keys in runtime.
Then I go and try to decode json that looks like this:

{
  "first_name" : "Ilya",
  "last_name" : "Puchka"
}

This does not work though and fails with an error "No value associated with key last_name". This is first, in my opinion, misleading thing - the error says that there is no data for a key that is clearly there.

The decoding works only when I change my custom coding key's raw value to the value that is a result of a transformation applied by key decoding strategy to the corresponding json key. So in this case - "lastName"

    enum CodingKeys: String, CodingKey {
        case firstName
        case surname = "lastName"
    }

Which is the second counterintuitive thing here, as I would expect the raw value of the key to be the raw value of json key, not some transformed string.

Can it be considered a bug or a shortcoming of current implementation, or is it my understanding of keys decoding strategies which is wrong?

Funny that this came up pretty much right after I posted Handling edge cases in JSONEncoder/JSONDecoder key conversion strategies (SR-6629). This is a good data point for discussing the topic, I think.

What you're seeing here is a combination of two things:

  1. The raw value of the key is affected by the key strategy. Anything consuming a CodingKey can only operate on its stringValue/intValue; there's no way for it to know that these values are being provided directly vs. being synthesized. As far as JSONEncoder/JSONDecoder is concerned, there's no difference between case last_name and case surname = "last_name", because there's no way for it to discern the difference
  2. The behavior on decode with the key decoding strategy is described in the linked thread — when you decode(String.self, forKey: .surname), the key in the JSON ("last_name") is converted to camelCase based on the .keyDecodingStrategy, and the string value of .surname ("last_name") is used to look up the value. Because the JSON payload has already been converted to camelCase "last_name" isn't found. Instead, you have to provide the camelCase name

(1) here happens by design (this is the idea behind the key strategy), but we're looking to improve on (2). Please take a look at the linked thread if you get a chance; I'd value your feedback there.