django-json-api/django-rest-framework-json-api

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 obj

Checklist

  • [ 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.