Can I losslessly recover integer keys which json.dumps() has converted to strings?

1.5k Views Asked by At

Consider this snippet:

>>> import json
>>> a = {1:'1'}
>>> json_a = json.dumps(a)
>>> json.loads(json_a)
{'1': '1'}

My attempt was to pass a Python dict to json.dumps(), and then use json.loads() to get it back. But that doesn't happen, probably because JSON alays considers keys as strings.

Is there any other way I can retain the original key types?

2

There are 2 best solutions below

2
Ahasanul Haque On

We can use str(value) in place of json.dumps(value), and ast.literal_eval() in place of json.loads()

>>> a = {1: "1"}
>>> a_as_string = str(a)
>>> import ast 
>>> ast.literal_eval(a_as_string)
{1: '1'}

Just posted as an initial solution, expecting a more sophisticated one.

4
Zero Piraeus On

The conversion of integer keys in Python dicts to JSON-compliant string keys by json.dumps() is lossy: once done, there's no way to tell whether the original key was the integer 23 or the string '23' (unless that information is stored elsewhere).

That said, you can force json.loads() to convert keys into integers wherever possible, by passing an appropriate function as an object_pairs_hook argument:

 def int_keys(ordered_pairs):
    result = {}
    for key, value in ordered_pairs:
        try:
            key = int(key)
        except ValueError:
            pass
        result[key] = value
    return result

Usage:

>>> import json
>>> data = {1: '1', 2: '2', 3: '3'}
>>> text = json.dumps(data)
>>> text
'{"1": "1", "2": "2", "3": "3"}'
>>> json.loads(text, object_pairs_hook=int_keys)
{1: '1', 2: '2', 3: '3'}

Expanding on this, it's also possible to write an object_pairs_hook which converts not only integers, but all the other non-string keys which json.dumps() might have converted to strings:

SPECIAL = {
    "true": True,
    "false": False,
    "null": None,
}

def round_trip(ordered_pairs):
    result = {}
    for key, value in ordered_pairs:
        if key in SPECIAL:
            key = SPECIAL[key]
        else:
            for numeric in int, float:
                try:
                    key = numeric(key)
                except ValueError:
                    continue
                else:
                    break
        result[key] = value

Usage:

>>> print(more_text)
{
  "2": 2,
  "3.45": 3.45,
  "true": true,
  "false": false,
  "null": null,
  "Infinity": Infinity,
  "-Infinity": -Infinity,
  "NaN": NaN
}
>>> json.loads(more_text, object_pairs_hook=round_trip)
{2: 2, 3.45: 3.45, True: True, False: False, None: None, inf: inf, -inf: -inf, nan: nan}