Recursively Modifying Python Data Structures

My Ansible Meraki modules send the raw responses from Meraki back to the playbook. However, Ansible’s standard is to use snake case formatting instead of camel case like Meraki does. Snake case uses underscores (ex. admin_id) and camel case uses capital letters (ex. adminId). To be consistent with other Ansible modules and the parameters in the modules, Ansible 2.9 will convert rewrite all the keys in Meraki’s response from camel case to snake case.

Meraki returns JSON which means I need to work with a mixture of strings, integers, floating point, dictionary, and list types.

I create a dictionary of camel case to snake case mappings. It may look like this.

key_mapping = {‘adminId’: ‘admin_id’,
               ‘name’: ‘name’, 

I’m including name in the mapping even though it doesn’t change so I have the ability to rename keys if I need.

My first attempts at the conversion code were based on small test cases which only used dictionaries. I tried to use a for loop and use the pop() method to remove the key/value pair from the old data structure and rename it to the new data structure. pop() removes the key from the dictionary so I received errors in the for loop stating the dictionary changed size while looping.

My next attempt at an algorithm used a simple for loop to create a new data structure.

for k, v in data.items():
    new_data[key_mapping[k]] = v

This would work fine if the dictionary was only one level deep and I didn’t need to use lists. This limitation lead me to believe recursion would be the proper solution.

Python has the isinstance() function which returns a boolean value stating if the object matches a type. For example

digit = 4
if isinstance(digit, int):

This code would print Integer. isinstance() can be used in a recursive function to take different actions based on the object type. After much experimentation (recursion is hard) I came up with a recursive algorithm which renames data.

def sanitize_keys(data):
    if isinstance(data, dict):
        items = {}
        for k, v in data.items():
            new = { key_map[k]: data[k] }
            items[key_map[k]] = sanitize_keys(data[k])
       return items
    elif isinstance(data, list):
        items = []
        for i in data:
        return items
    elif isinstance(data, int) or isinstance(data, str) or isinstance(data, float):
        return data

The easiest part to understand is the last elif. If the value of the particular data structure is an integer, string, or floating point type, there’s no further recursion possible so it only needs to return itself to the calling iteration.

Iterating through dictionaries and lists are handled different for two reasons. First, the dictionary requires the key and value to be pulled using items(). Second, the data structures have to be constructed in their same type. In other words, lists need to return lists and dictionaries need to return dictionaries.

For someone with some Python experience, this code snippet should be relatively straight forward. However, there is a corner case which needs to be handled. If Meraki were to add a key/value pair in their response, this function would exit with a KeyError exception. I rewrote the for loop to handle this case.

    new = { key_map[k]: data[k] }
    items[key_map[k]] = sanitize_keys(data[k])
except KeyError:
    snake_k = re.sub('([a-z0-9])([A-Z])', r'\1_\2', k).lower()
    new = { snake_k: data[k] }
    items[snake_k] = sanitize_keys(data[k])                

This stanza works by defaulting to the key mapping dictionary. But if the key is not found, I employ a regular expression to replace all capital letters and numbers, which aren’t the first character of the string, with lower case values and prepend an underscore. More specifically, it only prepends the underscore and lower() converts to lowercase. I could do the conversion using only regular expressions and not needing a key map. However, that would not allow me to rename keys which is a feature I periodically need.

The code hasn’t been submitted as a pull request. But I expect this implementation, or a very similar one, will be merged. This should work for more use cases than just the Meraki data structure. Feel free to use this code for your project.