Encoding/Decoding a Swift Dictionary to/from JSON

I am receiving JSON that looks like this:

{
  "1" : "Object 1",
  "2" : "Object 2",
  "3" : "Object 3"
}

And am trying to decode a Swift Dictionary [Int32 : String] using JSONDecoder. I tried this out in a playground quickly, using:

let d = """
    {
      "1" : "Object 1",
      "2" : "Object 2",
      "3" : "Object 3"
    }
""".data(using: .utf8)!
let y = try! JSONDecoder().decode([Int : Double].self, from: d)

This was successful, as was decoding [String : String]. In my app when the dictionary key was actually Int32 this failed. Upon further investigation, with the exception of a few key types, dictionaries tend to be encoded as and an array of interleaved key/value pairs. i.e:

let x: [Int32 : String] = [
  1 : "Object 1",
  2 : "Object 2",
  3 : "Object 3"
]
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let data = try! encoder.encode(x)
let jsonString = String(data: data, encoding: .utf8)!
print(jsonString)
/*
 [
  2,
  "Object 2",
  3,
  "Object 3",
  1,
  "Object 1"
]
*/

Why does [Int : String] encode as a json dictionary, but [Int32 : String] encode as an array?

Dictionary itself specifically treats Int and String keys differently from the rest. In particular, dictionaries with Int or String keys are treated as JSON objects, while all other dictionaries are treated as arrays of key-value pairs. IIRC it was an oversight not to allow more general types, which we're more or less stuck with given source compatibility.

Specifically, it treats Int and String as CodingKeys with the equivalent intValue/stringValue which allows it to encode as an actual dictionary as opposed to the more general types.

From the last time @Lantua and I discussed this (:sweat_smile:), see the links below for some more background on this:

If you need to specifically maintain a dictionary format here, your best bet is to encode and decode as [Int: String] but perform the conversion to Int32 afterwards.

I see. Thanks for in the info. It's unfortunate it doesn't look for a conformance to CodingKey to determine whether or not to encode as an actual dictionary.

The moral of the story: always pay very close attention to specifications, even if you think you know what you want to do. And always consider whether you actually want a concrete type, or if an existing protocol would more accurately describe what you want.

According to RFC 7159, JSON objects always use strings as keys. Decoding the keys to Int directly really shouldn’t be permitted. This requirement is reflected in the CodingKey protocol: conforming types all have a designated stringValue.

I'm not sure what you mean, the issue I, and others as linked by itaiferber are encountering is undocumented behaviour. It's not unreasonable that a Swift Dictionary encodes as a JSON object when Dictionary.Key conforms to CodingKey which requires a String representation, but unfortunately that is not the case.

UPDATE: a great summary of the original decision

AFAIK, source compatibility is not sacred if the source compatible behaviour is unintended, unexpected or a plain oversight.

1 Like