format_field_names does not supported nested structures
gbouzioto opened this issue · 3 comments
Description of the Bug Report
Hello I have observed that rest_framework_json_api.parsers.format_field_names does not support nested structures.
Unittest code to reproduce
import unittest
from rest_framework_json_api.utils import format_field_names
class TestCase(unittest.TestCase):
def test_format_field_names(self):
"""Tests for format_field_names"""
format_type = 'underscore'
# not a dictionary
obj = ['Foo', 'Bar']
res = format_field_names(obj, format_type)
self.assertIs(res, obj)
# simple dict
obj = {'camelCase': 'anExample'}
expected = {'camel_case': 'anExample'}
res = format_field_names(obj, format_type)
self.assertEqual(res, expected)
# list of dicts
obj = {'camelCase': [{'camelCase': 'anExample'}]}
expected = {'camel_case': [{'camel_case': 'anExample'}]}
res = format_field_names(obj, format_type)
self.assertEqual(res, expected)
# nested dicts
obj = {'camelCase': {'camelCase': {'camelCase': {'camelCase': 'anExample'}}}}
expected = {'camel_case': {'camel_case': {'camel_case': {'camel_case': 'anExample'}}}}
res = format_field_names(obj, format_type)
self.assertEqual(res, expected)
# complex dicts
obj = {
'camelCase': {
'camelCase': [
{'camelCase': [
{'camelCase': 'anExample'},
{'camelCase': 'anExample'},
{'camelCase': [
{'camelCase': 'anExample'},
{'camelCase': [
{'camelCase': 'anExample'},
{'camelCase': 'anExample'}
]
}
]
}
]
}
]
}
}
expected = {
'camel_case': {
'camel_case': [
{'camel_case': [
{'camel_case': 'anExample'},
{'camel_case': 'anExample'},
{'camel_case': [
{'camel_case': 'anExample'},
{'camel_case': [
{'camel_case': 'anExample'},
{'camel_case': 'anExample'}
]
}
]
}
]
}
]
}
}
res = format_field_names(obj, format_type)
self.assertEqual(res, expected)Suggested Solution
def format_field_names(obj, format_type=None):
"""It now supports conversion of nested dictionaries or list of dictionaries"""
if format_type is None:
format_type = json_api_settings.FORMAT_FIELD_NAMES
if isinstance(obj, dict):
formatted = OrderedDict()
for key, value in obj.items():
key = format_value(key, format_type)
# nested dictionary
if isinstance(value, dict):
value = custom_format_field_names(value, format_type)
# list of dicts
elif isinstance(value, list):
value = [custom_format_field_names(item, format_type) for item in value]
formatted[key] = value
return formatted
return objChecklist
- [ x] Certain that this is a bug (if unsure or you have a question use discussions instead)
- [ x] Code snippet or unit test added to reproduce bug
Thanks for your request. This is actually not a bug but deliberately not recursive. You can find more details in following discussion #1057 I have pinned this discussion now as it is a common question. In case you have any more comments feel free to add it to that discussion.
Thanks a lot for your reply. It makes sense.
I had to fix an existing app with this custom format_field_names, but there was still a problem with parser, when using nested structures. Using version 4.3.0, this package's JSONParser will not use our custom format_field_names function by itself. So I solved this by subclassing it and importing our custom function into this playground.
from rest_framework_json_api.parsers import JSONParser
from rest_framework_json_api.settings import json_api_settings
from <your custom function location here> import format_field_names
def undo_format_field_names(obj):
"""
Takes a dict and undo format field names to underscore which is the Python convention
but only in case `JSON_API_FORMAT_FIELD_NAMES` is actually configured.
"""
if json_api_settings.FORMAT_FIELD_NAMES:
return format_field_names(obj, "underscore")
return obj
class RecursiveJSONParser(JSONParser):
"""
Custom JSONParser that uses our recursive format_field_names.
Method bodies are the same but the imported format_field_names function
is the difference.
"""
@staticmethod
def parse_attributes(data):
attributes = data.get("attributes") or dict()
return undo_format_field_names(attributes)Maybe there is some other way around. I will dig deeper if this is the correct solution, just wanted to put this in here for anyone who might find this useful.