0

I have a Python object tree I want to serialize. Its structure is something like this:

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

I wanna serialize such object tree so that it's formatted like this in a JSON representation:

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

In other words, I want mixed formatting based on the contents of the object being formatted.

I tried playing around with json.JSONEncoder by overriding its default method and using a custom class object for storing the special_object's contents so that the custom default is called, something like this:

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)

But it looks like JSONEncoder encodes the default's return value again, so the string I generate for my custom type gets serialized as a string, with the output being something like:

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

which is clearly not what I had in mind. I also tried overriding the JSONEncoder.encode method like some answers here suggest, but that method never gets invoked from within JSONEncoder itself.

I did find several approaches to solving seemingly similar problems, but the solutions presented there are way too monstrously looking and overly complex for a task like mine. Is there a way to achieve what I need in just one-two dozen lines of code without going too deep into JSONEncoder's intestines? It does not need to be a generic solution, just something I could fix quickly and which may work for a single case only.

Semisonic
  • 1,152
  • 8
  • 21

1 Answers1

1

Actually, you can do something like this, but it's ... you see how it looks.

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)

Test:

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))

Output:

{
    "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"}
}
Olvin Roght
  • 7,677
  • 2
  • 16
  • 35
  • It's not pretty, basically a manual labor, but in the absence of a better solution it will do. Thanks for the effort =) – Semisonic Jun 05 '19 at 12:03
  • @Semisonic, there is another "correct" solution with subclassing JSONEncoder and overriding couple of methods, but It will be easily 10 times more code. If somebody interested, how to do it properly, I can add it to post. – Olvin Roght Jun 05 '19 at 12:33
  • 1
    I think `indent = kwargs.pop("indent", 0)` would save you a couple of lines of code while making your intent more obvious. – Stop harming Monica Jun 05 '19 at 15:57
  • @OlvinRoght, if your "proper" solution does not require reverse engineering the JSONEncoder's internals, it would be interesting to see. But, judging by the answers to similar questions I found here, it doesn't seem possible. – Semisonic Jun 06 '19 at 12:23