0

I want to change certain values in a json file (nested dicts and arrays). I thought a handy way to do that would be to take advantage of the JSONDecoder.

However, it's not working as I'd expect it to. I've done this exact same approach for getting JSONEncoder to convert np.arrays to lists so it wouldn't break the encoder.

After not getting it to do what I wanted, I thought maybe to try the Decoder instead. Same issue, it never calls default for handling strings it seems. Maybe default is never called when handling a string, just when handling other types of objects?

# key, val are arguments passed in, e.g. ("bar", "2.0rc1")
# Replace the value "2.0rc1" everywhere the "bar" key is found

class StringReplaceDecoder(json.JSONDecoder):
    def default(self, obj):
        if isinstance(obj, str):
            print("Handling obj str: {}".format(obj))
            if obj == key:
                return val
        return json.JSONEncoder.default(self, obj)


json_dump = json.dumps(dict)
json_load = json.loads(json_dump, cls=StringReplaceDecoder)

# Example input
{a:{foo:"", bar:"1.3"}, b:{d:{foo:""}, z:{bar:"1.5"}}}
# Example desired output:
{a:{foo:"", bar:"2.0rc1"}, b:{d:{foo:""}, z:{bar:"2.0rc1"}}}
SwimBikeRun
  • 4,192
  • 11
  • 49
  • 85
  • After some testing and looking at the source, I'm almost sure the behavior is that default is never entered for string types. So that method that worked great for np.ndarray types is not going to work here. So is there another way to get custom behavior on particular strings? – SwimBikeRun Feb 21 '19 at 01:11
  • You could create your own "string-like" type (not derived from `str`) and override what the default encoder does when an instance of one of them is encountered. Also see [How to set the number of float digits JSONEncoder produces?](https://stackoverflow.com/questions/54370322/how-to-set-the-number-of-float-digits-jsonencoder-produces). – martineau Feb 21 '19 at 01:30
  • 2
    Firstly, your example subclasses `JSONDecoder`, which does not have a `default` method (because it only parses json-encoded strings). Secondly, the `default` method of `JSONEncoder` is only called for objects which ***aren't*** automatically supported (so not for `str`, `int`, `list`, etc). This is all explained pretty clearly in the [json docs](https://docs.python.org/3/library/json.html#json.JSONEncoder). I therefore suggest you forget about `json` and just write a simple recursive function that understands your particular data-structure. – ekhumoro Feb 21 '19 at 01:43
  • Ah thanks for that. Makes sense only the encoder has a default. – SwimBikeRun Feb 21 '19 at 03:15

1 Answers1

0

After finding a solution that worked, I found a far superior one liner that didn't come up in previous google searches.

The correct answer for my problem is from nested_lookup import nested_update

For what it's worth, I also found the object_hook did exactly what I wanted as well:

def val_hook(obj):
    return_d = {}
    if isinstance(obj, dict):
        for k in obj:
            if in_key == k:
                return_d[k] = in_val
            else:
                return_d[k] = obj[k]
        return return_d
    else:
        return obj

json_dump = json.dumps(in_dict)
json_load = json.loads(json_dump, object_hook=val_hook)

References

SwimBikeRun
  • 4,192
  • 11
  • 49
  • 85
  • 1
    I find this all a bit baffling. Why resort to such hackery for something so simple? Sane solution: `def func(d): for k, v in d.items(): if in_key == k: d[k] = in_val; elif isinstance(v, dict): func(v)`. No need to use `json` at all. – ekhumoro Feb 21 '19 at 14:56
  • You are absolutely right for the stupid way I worded the opening question as "Nested dict". What I meant to say was valid json, so "nested dict/array". I edited the original question to reflect what I was really after. Sorry. And good tip, thanks! – SwimBikeRun Feb 23 '19 at 05:04