JSONDecoder enum-keyed dictionary bug

Hello everyone, I think I've discovered a bug in JSONDecoder and I'd like to get a quick sanity-check on this, as well as hopefully some ideas to work around the issue.

When attempting to decode a Decodable struct from JSON using JSONDecoder, the decoder throws an error when it encounters a dictionary that is keyed by an enum and somehow seems to think that the enum is an Array<Any>.

Here's a minimal reproducible example:

let jsonString = """
{
  "variations": {
    "plural": "tests",
    "singular": "test"
  }
}
"""

struct Json: Codable {
  let variations: [VariationKind: String]
}

enum VariationKind: String, Codable, Hashable {
  case plural = "plural"
  case singular = "singular"
}

and then the actual decoding:

let json = try JSONDecoder().decode(
  Json.self,
  from: jsonString.data(using: .utf8)!
)
print(json)

The expected result would of course be the following:

Json(
  variations: [
    VariationKind.plural: "tests",
    VariationKind.singular: "test"
  ]
)

But the actual result is an error:

Swift.DecodingError.typeMismatch(
  Swift.Array<Any>,
  Swift.DecodingError.Context(
    codingPath: [
      CodingKeys(stringValue: "variations", intValue: nil)
    ],
    debugDescription: "Expected to decode Array<Any> but found a dictionary instead.",
    underlyingError: nil
  )
)

So basically, the JSONDecoder tries to decode Swift.Array<Any> but encounters a dictionary (duh), and I have no idea why this is happening. There are literally no arrays anywhere, neither in the actual JSON string, nor in any of the Codable structs.

Curiously, if I change the dictionary from [VariationKind: String] to [String: String], everything works perfectly.

So something about the enum seems to cause confusion in JSONDecoder. I've tried to fix this by implementing Decodable myself for VariationKind and using a singleValueContainer, but that causes exactly the same error.

Am I crazy, or is that a bug?

Answered by Claude31 in 845223022

They explain here a subtle behaviour (to say the least) of JSONEncoder.

https://forums.swift.org/t/encoding-decoding-a-swift-dictionary-to-from-json/39989

A dictionary with Int or String key is encoded as dictionary.

But with other type key, it is encoded as an array. That seems to be the case with the enum.

I tested by catching error:

do {
    let json = try JSONDecoder().decode(Json.self, from: jsonString.data(using: .utf8)!)
    print("JSON", json)
} catch {
    print("Error", error)
}

and got the same error message:

Error typeMismatch(Swift.Array<Any>, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "variations", intValue: nil)], debugDescription: "Expected to decode Array<Any> but found a dictionary instead.", underlyingError: nil))

As you noted, if you replace the key type by String:

struct Json: Codable {
  let variations: [String: String]
}

Then you get the expected result:

JSON Json(variations: ["plural": "tests", "singular": "test"])

They explain here a subtle behaviour (to say the least) of JSONEncoder.

https://forums.swift.org/t/encoding-decoding-a-swift-dictionary-to-from-json/39989

A dictionary with Int or String key is encoded as dictionary.

But with other type key, it is encoded as an array. That seems to be the case with the enum.

I tested by catching error:

do {
    let json = try JSONDecoder().decode(Json.self, from: jsonString.data(using: .utf8)!)
    print("JSON", json)
} catch {
    print("Error", error)
}

and got the same error message:

Error typeMismatch(Swift.Array<Any>, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "variations", intValue: nil)], debugDescription: "Expected to decode Array<Any> but found a dictionary instead.", underlyingError: nil))

As you noted, if you replace the key type by String:

struct Json: Codable {
  let variations: [String: String]
}

Then you get the expected result:

JSON Json(variations: ["plural": "tests", "singular": "test"])
JSONDecoder enum-keyed dictionary bug
 
 
Q