177

I have found that when the following is run, Python's json module (included since 2.6) converts int dictionary keys to strings.

import json
releases = {1: "foo-v0.1"}
json.dumps(releases)

Output:

'{"1": "foo-v0.1"}'

Is there an easy way to preserve the key as an int, without needing to parse the string on dump and load?

I believe it would be possible using the hooks provided by the json module, but again this still requires parsing. Is there possibly an argument I have overlooked?

Sub-question: Thanks for the answers. Seeing as json works as I feared, is there an easy way to convey key type by maybe parsing the output of dumps?

Also I should note the code doing the dumping and the code downloading the JSON object from a server and loading it, are both written by me.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Charles Ritchie
  • 2,283
  • 2
  • 16
  • 21

9 Answers9

117

This is one of those subtle differences among various mapping collections that can bite you. JSON treats keys as strings; Python supports distinct keys differing only in type.

In Python (and apparently in Lua) the keys to a mapping (dictionary or table, respectively) are object references. In Python they must be immutable types, or they must be objects which implement a __hash__ method. (The Lua docs suggest that it automatically uses the object's ID as a hash/key even for mutable objects and relies on string interning to ensure that equivalent strings map to the same objects).

In Perl, JavaScript, awk and many other languages the keys for hashes, associative arrays or whatever they're called for the given language, are strings (or "scalars" in Perl). In Perl, $foo{1}, $foo{1.0}, and $foo{"1"} are all references to the same mapping in %foo --- the key is evaluated as a scalar!

JSON started as a JavaScript serialization technology. (JSON stands for JavaScript Object Notation.) Naturally it implements semantics for its mapping notation which are consistent with its mapping semantics.

If both ends of your serialization are going to be Python then you'd be better off using pickles. If you really need to convert these back from JSON into native Python objects I guess you have a couple of choices. First you could try (try: ... except: ...) to convert any key to a number in the event of a dictionary look-up failure. Alternatively, if you add code to the other end (the serializer or generator of this JSON data) then you could have it perform a JSON serialization on each of the key values—providing those as a list of keys. (Then your Python code would first iterate over the list of keys, instantiating/deserializing them into native Python objects ... and then use those for access the values out of the mapping).

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Jim Dennis
  • 17,054
  • 13
  • 68
  • 116
  • 1
    Thanks for that. Unfortunately I can't use Pickle, but your idea with the list is great. Will implement that now, cheers for the idea. – Charles Ritchie Sep 21 '09 at 02:34
  • 1
    (Incidentally, in Python 1, 1L (long integer), and 1.0 map to the same key; but "1" (a string) does not map to the same as 1 (integer) or 1.0 (float) or 1L (long integer). – Jim Dennis Jun 23 '15 at 04:35
  • 7
    Be cautious with the recommendation of using Pickle. Pickle can result in arbitrary code execution, so if the source of the data you're deserializing isn't inherently trustworthy, you should stick to a "safe" serialization protocol like JSON. Also keep in mind that as scope of projects expand, sometimes functions that you expected would only get trusted input start getting user provided input, and the security considerations aren't always revisited. – AusIV Jun 24 '16 at 15:47
59

No, there is no such thing as a Number key in JavaScript. All object properties are converted to String.

var a= {1: 'a'};
for (k in a)
    alert(typeof k); // 'string'

This can lead to some curious-seeming behaviours:

a[999999999999999999999]= 'a'; // this even works on Array
alert(a[1000000000000000000000]); // 'a'
alert(a['999999999999999999999']); // fail
alert(a['1e+21']); // 'a'

JavaScript Objects aren't really proper mappings as you'd understand it in languages like Python, and using keys that aren't String results in weirdness. This is why JSON always explicitly writes keys as strings, even where it doesn't look necessary.

bobince
  • 528,062
  • 107
  • 651
  • 834
  • 1
    Why isn't `999999999999999999999` converted to `'999999999999999999999'`? – Piotr Dobrogost Oct 07 '16 at 08:05
  • 5
    @PiotrDobrogost JavaScript (like many languages) can't store arbitrarily-large numbers. The `Number` type is an [IEEE 754 double](https://en.wikipedia.org/wiki/IEEE_floating_point) floating point value: you get 53 bits of mantissa, so you can store up to 2⁵³ (9007199254740992) with integer accuracy; beyond that integers will round to other values (hence 9007199254740993 === 9007199254740992). 999999999999999999999 rounds to 1000000000000000000000, for which the default `toString` representation is `1e+21`. – bobince Oct 08 '16 at 09:27
31

Answering your subquestion:

It can be accomplished by using json.loads(jsonDict, object_hook=jsonKeys2int)

def jsonKeys2int(x):
    if isinstance(x, dict):
        return {int(k):v for k,v in x.items()}
    return x

This function will also work for nested dicts and uses a dict comprehension.

If you want to to cast the values too, use:

def jsonKV2int(x):
    if isinstance(x, dict):
        return {int(k):(int(v) if isinstance(v, unicode) else v) for k,v in x.items()}
    return x

Which tests the instance of the values and casts them only if they are strings objects (Unicode to be exact).

Both functions assumes keys (and values) to be integers.

Thanks to:

How can I use if/else in a dictionary comprehension?

Convert a string key to int in a Dictionary

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Murmel
  • 5,402
  • 47
  • 53
  • 1
    This was great. In my case pickling can't be used so I'm saving the guts of an object using JSON via conversion to a byte_array so that I can use compression. I have got mixed keys, so I just modified your example to ignore a ValueError when the key is not convertible to an int – minillinim Oct 24 '18 at 04:35
  • this will only work if you want all of your keys to be ints though right? If OP throws in a key that isn't convertible to `int`, this will throw a ValueError – physincubus Apr 29 '21 at 21:03
  • Right, hence the last sentence about this assumption. – Murmel Apr 30 '21 at 11:38
23

Alternatively you can also try converting dictionary to a list of [(k1,v1),(k2,v2)] format while encoding it using JSON, and converting it back to dictionary after decoding it back.


>>>> import json
>>>> json.dumps(releases.items())
    '[[1, "foo-v0.1"]]'
>>>> releases = {1: "foo-v0.1"}
>>>> releases == dict(json.loads(json.dumps(releases.items())))
     True

I believe this will need some more work like having some sort of flag to identify what all parameters to be converted to dictionary after decoding it back from JSON.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Ashish
  • 430
  • 7
  • 13
11

I've gotten bitten by the same problem. As others have pointed out, in JSON, the mapping keys must be strings. You can do one of two things. You can use a less strict JSON library, like demjson, which allows integer strings. If no other programs (or no other in other languages) are going to read it, then you should be okay. Or you can use a different serialization language. I wouldn't suggest pickle. It's hard to read, and is not designed to be secure. Instead, I'd suggest YAML, which is (nearly) a superset of JSON, and does allow integer keys. (At least PyYAML does.)

John Zwinck
  • 239,568
  • 38
  • 324
  • 436
AFoglia
  • 7,968
  • 3
  • 35
  • 51
10

Here is my solution! I used object_hook, and it is useful when you have nested JSON content.

>>> import json
>>> json_data = '{"1": "one", "2": {"-3": "minus three", "4": "four"}}'
>>> py_dict = json.loads(json_data, object_hook=lambda d: {int(k) if k.lstrip('-').isdigit() else k: v for k, v in d.items()})

>>> py_dict
{1: 'one', 2: {-3: 'minus three', 4: 'four'}}

There is a filter only for parsing the json key to int. You can use int(v) if v.lstrip('-').isdigit() else v filter for the json value too.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
GooDeeJAY
  • 1,681
  • 2
  • 20
  • 27
3

Convert the dictionary to be string by using str(dict) and then convert it back to dict by doing this:

import ast
ast.literal_eval(string)
Hzzkygcs
  • 1,532
  • 2
  • 16
  • 24
3

I made a very simple extension of Murmel's answer which I think will work on a pretty arbitrary dictionary (including nested) assuming it can be dumped by JSON in the first place. Any keys which can be interpreted as integers will be cast to int. No doubt this is not very efficient, but it works for my purposes of storing to and loading from JSON strings.

def convert_keys_to_int(d: dict):
    new_dict = {}
    for k, v in d.items():
        try:
            new_key = int(k)
        except ValueError:
            new_key = k
        if type(v) == dict:
            v = _convert_keys_to_int(v)
        new_dict[new_key] = v
    return new_dict

Assuming that all keys in the original dict are integers if they can be cast to int, then this will return the original dictionary after storing as a JSON file.

E.g.,

>>>d = {1: 3, 2: 'a', 3: {1: 'a', 2: 10}, 4: {'a': 2, 'b': 10}}
>>>convert_keys_to_int(json.loads(json.dumps(d)))  == d
True
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Tim Child
  • 339
  • 1
  • 11
  • This is really good, except it doesn't work where the dict contains lists which contain more dicts. – KyferEz Sep 01 '23 at 19:37
-1

[NSFW]. You can write your json.dumps by yourself. Jere is a example from djson: encoder.py. You can use it like this:

assert dumps({1: "abc"}) == '{1: "abc"}'
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
damnever
  • 12,435
  • 1
  • 13
  • 18