Format certain JSON objects on one line
<p>Consider the following code:</p>

<pre><code>&gt;&gt;&gt; import json
&gt;&gt;&gt; data = {
...     'x': [1, {'$special': 'a'}, 2],
...     'y': {'$special': 'b'},
...     'z': {'p': True, 'q': False}
... }
&gt;&gt;&gt; print(json.dumps(data, indent=2))
{
  "y": {
    "$special": "b"
  },
  "z": {
    "q": false,
    "p": true
  },
  "x": [
    1,
    {
      "$special": "a"
    },
    2
  ]
}
</code></pre>

<p>What I want is to format the JSON so that JSON objects that have only a single property <code>'$special'</code> are rendered on a single line, as follows.</p>

<pre><code>{
  "y": {"$special": "b"},
  "z": {
    "q": false,
    "p": true
  },
  "x": [
    1,
    {"$special": "a"},
    2
  ]
}
</code></pre>

<p>I have played around with implementing a custom <a href="https://docs.python.org/3/library/json.html#json.JSONEncoder" rel="nofollow"><code>JSONEncoder</code></a> and passing that in to <code>json.dumps</code> as the <code>cls</code> argument, but the two methods on <code>JSONEncoder</code> each have a problem:</p>

<ul>
<li><p>The <code>JSONEncoder</code> <a href="https://docs.python.org/3/library/json.html#json.JSONEncoder.default" rel="nofollow"><code>default</code></a> method is called for each part of <code>data</code>, but the return value is not a raw JSON string, so there doesn't appear to be any way to adjust its formatting.</p></li>
<li><p>The <code>JSONEncoder</code> <a href="https://docs.python.org/3/library/json.html#json.JSONEncoder.encode" rel="nofollow"><code>encode</code></a> method does return a raw JSON string, but it is only called once for the <code>data</code> as a whole.</p></li>
</ul>

<p>Is there any way I can get <code>JSONEncoder</code> to do what I want?</p>

<p>The <code>json</code> module is not really designed to give you that much control over the output; indentation is mostly meant to aid readability while debugging.</p>

<p>Instead of making <code>json</code> produce the output, you could <em>transform</em> the output using the standard library <a href="https://docs.python.org/3/library/tokenize.html" rel="nofollow"><code>tokenize</code> module</a>:</p>

<pre><code>import tokenize
from io import BytesIO


def inline_special(json_data):
    def adjust(t, ld,):
        """Adjust token line number by offset"""
        (sl, sc), (el, ec) = t.start, t.end
        return t._replace(start=(sl + ld, sc), end=(el + ld, ec))

    def transform():
        with BytesIO(json_data.encode('utf8')) as b:
            held = []  # to defer newline tokens
            lastend = None  # to track the end pos of the prev token
            loffset = 0     # line offset to adjust tokens by
            tokens = tokenize.tokenize(b.readline)
            for tok in tokens:
                if tok.type == tokenize.NL:
                    # hold newlines until we know there's no special key coming
                    held.append(adjust(tok, loffset))
                elif (tok.type == tokenize.STRING and
                        tok.string == '"$special"'):
                    # special string, collate tokens until the next rbrace
                    # held newlines are discarded, adjust the line offset
                    loffset -= len(held)
                    held = []
                    text = [tok.string]
                    while tok.exact_type != tokenize.RBRACE:
                        tok = next(tokens)
                        if tok.type != tokenize.NL:
                            text.append(tok.string)
                            if tok.string in ':,':
                                text.append(' ')
                        else:
                            loffset -= 1  # following lines all shift
                    line, col = lastend
                    text = ''.join(text)
                    endcol = col + len(text)
                    yield tokenize.TokenInfo(
                        tokenize.STRING, text, (line, col), (line, endcol),
                        '')
                    # adjust any remaining tokens on this line
                    while tok.type != tokenize.NL:
                        tok = next(tokens)
                        yield tok._replace(
                            start=(line, endcol),
                            end=(line, endcol + len(tok.string)))
                        endcol += len(tok.string)
                else:
                    # uninteresting token, yield any held newlines
                    if held:
                        yield from held
                        held = []
                    # adjust and remember last position
                    tok = adjust(tok, loffset)
                    lastend = tok.end
                    yield tok

    return tokenize.untokenize(transform()).decode('utf8')
</code></pre>

<p>This reformats your sample successfully:</p>

<pre><code>import json

data = {
    'x': [1, {'$special': 'a'}, 2],
    'y': {'$special': 'b'},
    'z': {'p': True, 'q': False}
}

&gt;&gt;&gt; print(inline_special(json.dumps(data, indent=2)))
{
  "x": [
    1,
    {"$special": "a"},
    2
  ],
  "y": {"$special": "b"},
  "z": {
    "p": true,
    "q": false
  }
}
</code></pre>

python json python-3.x formatting