0

I want to re-format a JSON file so that certain objects (dictionaries) with some specific keys are on one-line.

For example, any object with key name should appear in one line:

{
  "this": "that",
  "parameters": [
    { "name": "param1", "type": "string" },
    { "name": "param2" },
    { "name": "param3", "default": "@someValue" }
  ]
}

The JSON file is generated, and contains programming language data. One-line certain fields makes it much easier to visually inspect/review.

I tried to override python json.JSONEncoder to turn matching dict into a string before writing, only to realize quotes " within the string are escaped again in the result JSON file, defeating my purpose.

I also looked at jq but couldn't figure out a way to do it. I found similar questions and solutions based on line length, but my requirements are simpler, and I don't want other shorter lines to be changed. Only certain objects or fields.

raychi
  • 2,423
  • 1
  • 21
  • 12
  • 1
    Is json.dumps(parsed, indent=4, sort_keys=True) pretty enough? – Bober Nov 06 '19 at 19:11
  • 4
    Overriding `json.JSONEncoder` would work. Suggest you put your attempt into your question because whatever issue you had could probably be fixed. Also see my answer to [How to implement custom indentation when pretty-printing with the JSON module?](https://stackoverflow.com/questions/13249415/how-to-implement-custom-indentation-when-pretty-printing-with-the-json-module) – martineau Nov 06 '19 at 19:31

1 Answers1

3

This code recursively replaces all the appropriate dicts in the data with unique strings (UUIDs) and records those replacements, then in the indented JSON string the unique strings are replaced with the desired original single line JSON.

replace returns a pair of:

  • A modified version of the input argument data
  • A list of pairs of JSON strings where for each pair the first value should be replaced with the second value in the final pretty printed JSON.
import json
import uuid


def replace(o):
    if isinstance(o, dict):
        if "name" in o:
            replacement = uuid.uuid4().hex
            return replacement, [(f'"{replacement}"', json.dumps(o))]
        replacements = []
        result = {}
        for key, value in o.items():
            new_value, value_replacements = replace(value)
            result[key] = new_value
            replacements.extend(value_replacements)
        return result, replacements
    elif isinstance(o, list):
        replacements = []
        result = []
        for value in o:
            new_value, value_replacements = replace(value)
            result.append(new_value)
            replacements.extend(value_replacements)
        return result, replacements
    else:
        return o, []


def pretty(data):
    data, replacements = replace(data)
    result = json.dumps(data, indent=4)
    for old, new in replacements:
        result = result.replace(old, new)
    return result


print(pretty({
    "this": "that",
    "parameters": [
        {"name": "param1", "type": "string"},
        {"name": "param2"},
        {"name": "param3", "default": "@someValue"}
    ]
}))

Alex Hall
  • 34,833
  • 5
  • 57
  • 89
  • Nice! Works well! Do you mind adding some basic explanation? This will be helpful for others who might wants to extend it to other use cases. For example, I want to also one-line certain field values (in addition to certain dictionaries). Thanks! – raychi Nov 07 '19 at 20:18