Пользовательское форматирование только части данных во время сериализации JSON - PullRequest
0 голосов
/ 05 июня 2019

У меня есть дерево объектов Python, которое я хочу сериализовать. Его структура примерно такая:

{
    "regular_object": {
        ...
    },
    "regular_field": ...
    "special_object": {
        "f1": "v1",
        "f2": v2,
        ...
        "fn": "vn"
    }
}

Я хочу сериализовать такое дерево объектов, чтобы оно форматировалось в виде JSON:

{
    "regular_object": {
        ...
    },
    "regular_field": ...
    "special_object": { "f1": "v1", "f2": v2, ..., "fn": "vn" }
}

Другими словами, я хочу смешанное форматирование на основе содержимого форматируемого объекта .

Я попытался поиграться с json.JSONEncoder, переопределив его метод default и используя объект пользовательского класса для хранения содержимого special_object, чтобы вызывался пользовательский default, примерно так:

class SpecialObject:
    def __init__(self, attrs: dict):
        self.attrs = attrs

class SpecialEncoder(json.JSONEncoder):
    def default(self, o):
        JE = json.JSONEncoder
        if isinstance(o, SpecialObject):
            return f"{{ ', '.join([f'{JE.encode(self, el[0])}: {JE.encode(el[1])}' for el in o.attrs.items()]) }}"
        else:
            return super().default(o)

Но похоже, что JSONEncoder кодирует возвращаемое default возвращаемое значение снова , поэтому строка, которую я генерирую для моего пользовательского типа, сериализуется в виде строки, с выводом, похожим на:

{
    ...
    "special_object": "{ \"f1\": \"v1\", ..., \"fn\": \"vn\" }"
}

что явно не то, что я имел в виду. Я также попытался переопределить метод JSONEncoder.encode, как показывают некоторые ответы, но этот метод никогда не вызывается из самого JSONEncoder.

Я нашел несколько подходов к решению, казалось бы, похожих проблем, но представленные там решения слишком чудовищно выглядят и слишком сложны для такой задачи, как моя. Есть ли способ достичь того, что мне нужно, всего за одну-две дюжины строк кода, не вдаваясь слишком глубоко в кишечник JSONEncoder? Это не должно быть универсальное решение, просто что-то, что я мог бы быстро исправить и которое может работать только для одного случая.

1 Ответ

1 голос
/ 05 июня 2019

На самом деле, вы можете сделать что-то вроде этого, но это ... вы видите, как это выглядит.

def custom_json_dumps(obj, *args, keys_to_skip_indent=(), **kwargs):
    if isinstance(obj, dict):
        indent = kwargs.pop("indent", 0)
        separators = kwargs.get("separators", (", ", ": "))

        res = "{" + "\n" * int(indent != 0)
        for k, v in obj.items():
            if k in keys_to_skip_indent or indent == 0:
                encoded = json.dumps(v, *args, **kwargs)
            else:
                encoded = json.dumps(v, *args, indent=indent, **kwargs)
            res += "\"{}\"".format(k) + separators[1] + encoded + separators[0] + "\n" * int(indent != 0)

        return res[:res.rindex(separators[0])].replace("\n", "\n" + " " * indent) + "\n" * int(indent != 0) + "}"
    else:
        return json.dumps(obj, *args, **kwargs)

Тест:

o = {
    "regular_object": {
        "a": "b"
    },
    "regular_field": 100000,
    "float_test": 1.0000001,
    "bool_test": True,
    "list_test": ["1", 0, 1.32, {"a": "b"}],
    "special_object": {
        "f1": "v1",
        "f2": "v2",
        "fn": "vn"
    }
}

print(custom_json_dumps(o, keys_to_skip_indent={"special_object",  "list_test"}, indent=4))

Выход:

{
    "regular_object": {
        "a": "b"
    }, 
    "regular_field": 100000, 
    "float_test": 1.0000001, 
    "bool_test": true, 
    "list_test": ["1", 0, 1.32, {"a": "b"}], 
    "special_object": {"f1": "v1", "f2": "v2", "fn": "vn"}
}
...