3

JSON only allows strings as keys.

The code below uses a custom JSONEncoder to turn Decimal values into strings.

Is there a way to specify an encoder that will turn Decimal keys into strings?

import json
import decimal

class DecimalEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, decimal.Decimal):
            return str(obj)
        return json.JSONEncoder.default(self, obj)

d1 = {3: decimal.Decimal(50)}
print(json.dumps(d1, cls=DecimalEncoder))

d2 = {decimal.Decimal(50): 3}
json.dumps(d2, cls=DecimalEncoder)  # TypeError: keys must be a string

I am using python3.6.

Note: Obviously I could iterate through my dictionary and replace the Decimal types with string values, but I am hoping to find a more elegant solution perhaps by adding behaviour to the encoder.

1 Answer 1

1

No, you can't hook the handling of keys, you'll have to convert them before you encode. You can do so with a recursive handler like:

from functools import singledispatch

@singledispatch
def string_keys(obj):
    return obj

@string_keys.register(dict)
def _(d):
    return {str(k): string_keys(v) for k, v in d.items()}

@string_keys.register(list)
def _(l):
    return [string_keys(v) for v in l]

All this does is convert a nested structure of lists and dicts recursively where all keys are forced to be strings.

Use this when converting to JSON:

json_encoded = json.dumps(string_keys(data))

You can extend this to handle Decimal objects (outside of keys) too by adding another registry:

@string_keys.register(Decimal)
def _(d):
    return str(d)

Going the other way is a little tricky, unless you explicitly mark up Decimal keys (with a prefix, say), you can't easily distinguish between keys that were strings to start with and Decimal values. You could use a try/except approach here:

from functools import singledispatch

@singledispatch
def keys_to_decimal(obj):
    return obj

@keys_to_decimal.register(dict)
def _(d):
    def try_decimal(k):
        try:
            return Decimal(k)
        except ValueError:
            return k
    return {try_decimal(k): keys_to_decimal(v) for k, v in d.items()}

@keys_to_decimal.register(list)
def _(l):
    return [keys_to_decimal(v) for v in l]

Demo:

>>> string_keys([{Decimal(0): 'foo'}])
[{'0': 'foo'}]
>>> keys_to_decimal(string_keys([{Decimal(0): 'foo'}]))
[{Decimal('0'): 'foo'}]
Sign up to request clarification or add additional context in comments.

2 Comments

I am getting this error NameError: name 'k' is not defined in the line return {str(k): string_keys(v) for v in d} .
@ChrisGuest: and another mistake in the keys_to_decimal handling; now actually tested. :-)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.