Should json restrict string indentation to JSON whitespace?

The json module accepts arbitrary strings for indent. Depending on the string and input data, this can produce invalid JSON or valid JSON with different values.

For example:

import json

data = [1, 2]

for indent in ["4", "1", "-", "1e", "1."]:
    text = json.dumps(data, indent=indent)
    print(repr(indent), json.loads(text))
'4' [41, 42]
'1' [11, 12]
'-' [-1, -2]
'1e' [10.0, 100.0]
'1.' [1.1, 1.2]

For a typical object such as {"name": "example", "values": [1, 2]}, those same strings produce invalid JSON instead. This was reproduced on CPython 3.14.5 on Windows, including the Python encoder fallback.

Reproducible test

The test compares against serialization without indentation, excluding normal JSON conversions from the comparison.

import json

documents = {
    "object": {
        "name": "example",
        "active": True,
        "values": [1, 2],
        "metadata": None,
    },
    "numeric_array": [1, 2],
}

indents = [
    None, 4, 0, -1, "", " ", "\t", "\n", "\r",
    " \t\n\r", "4", "0", "1", " 4", "-", "-1",
    "1e", "1.", "0.", "banana", "true", "null",
    "\f", "\v", "\u00a0", "\u2003",
]

for name, data in documents.items():
    baseline = json.loads(json.dumps(data))

    for indent in indents:
        text = json.dumps(data, indent=indent)

        try:
            decoded = json.loads(text)
        except json.JSONDecodeError:
            status = "invalid JSON"
        else:
            status = (
                "unchanged"
                if decoded == baseline
                else f"changed to {decoded!r}"
            )

        print(f"{name}: indent={indent!r}: {status}")

Proposal and compatibility

bpo-41998 / GH-86164 previously reported field injection through indent and proposed converting it to an integer. It was closed as “not a bug,” with validation considered the application’s responsibility.

Would a narrower change be worth considering: retain string indentation, but reject characters outside JSON’s whitespace set—space, tab, LF and CR—as defined in RFC 8259?

Empty strings, combinations of those four characters, integers and None would remain supported. str.isspace() would be too permissive because it accepts additional characters outside JSON’s whitespace set.

I recognize this would narrow documented behavior. Are there use cases for non-whitespace indentation that make this restriction undesirable, or would preventing these effects justify a compatibility transition?

2 Likes

I do fully agree that validation is application’s responsibility, but IMO that does not imply that adding another line of defence is not a good idea.

According to the docs, the purpose of the indent argument is pretty-printing. There is no valid use-case for indenting with a non-whitespace string . It produces invalid output (1) or modified output, so it is at least an error. The linked issue was closed in 2020 as “not a bug”. That’s correct, but it would be nice if a patch would be accepted as an enahncement. In 2026 AI tools are searching for anything that could be misused like crazy.

____
Added note 1: later in the discussion it was argued that human readable “pretty” output does not need to be valid JSON. That’s a fair point.

I think we should not prevent this. The user is a responsive adult. If the user wants to use ***** for the indent, and explicitly provides indent="*****" that’s what the user explicitly asked for. It can be very frustrating when you want something unusual and the library maintainers decided to put in an assert that blocks you from doing what you want.

12 Likes

Are there use cases for non-whitespace indentation

Github search finds some cases like json.dumps(body, indent="| ").

9 Likes

I think restricting string indentation to JSON whitespace makes sense. Allowing arbitrary characters can unexpectedly change the parsed values, so rejecting non-whitespace characters would make indent much safer and more predictable without affecting normal formatting use cases.

The indent is almost always a string literal. It’s not like this is an externally-controlled attack or anything. Yes, you can use this to create something that isn’t valid JSON… congrats. There are already plenty of other ways to do that (in fact, allow_nan=True is the default, and is actually in violation of the spec); this is a pretty benign one.

3 Likes

I checked the Lambda example. It uses indent="| " for printing, while the SQS message and HTTP response use regular JSON output. I can see why restricting it would break a useful formatting choice.

What surprised me was that other indent strings can produce valid JSON with different numeric values. Would a short note in the docs about that behavior be worth adding?

Garbage In – Garbage Out.

It is not that passing non-whitespace indent is a common error. The indent argument is usually hardcoded. If it is dynamic – it is more convenient to use an integer. indent is used to produce more human readable output, and we see that there are use cases to produce human readable representation even if it is not JSON.

BTW, there is also the separators parameter. Normally, it only takes (', ', ': '), (',', ': '), or (',', ':'), but there is no check, and you can use arbitrary values and produce weird looking or invalid JSON. This is up to you.

3 Likes

If all examples use typical values that cause no problems, and it is already called indent, which to me implies whitespace. I think there should be a limit to protectionism, and that reading docs and trying things out are a part of learning the extent of a library.

“Doctor doctor, it hurts if I do this”
“Then don’t do it, then!”
:wink:

5 Likes

I’ve opened [json] Require a `LiteralString` for `indent` argument by srittau · Pull Request #16402 · python/typeshed · GitHub against typeshed. This should at least prevent users using type checkers from using non-literal strings for indent without an explicit “I know what I’m doing” cast.

3 Likes

How would you write a wrapper around json.dumps()?

How would you write something like indent='\t' if indent >= 8 else indent?

1 Like

Why restrict people? What’s the point? Is there an actual threat being protected against?

5 Likes

Why restrict people? What’s the point? Is there an actual threat being protected against?

The threat was described in the original post. This is not a restriction as it can easily be worked around using a cast. Should we remove LiteralString from SQL queries as well?

1 Like

How would you write a wrapper around json.dumps()?

How would you write something like indent='\t' if indent >= 8 else indent?

Was this directed at me? In that case I don’t understand the question or the problem.

Edit: Could you give a full example that you think is problematic? indent='\t' if indent >= 8 else indent isn’t, depending on how exactly indent is typed.

SQL injection is a very real and well-known threat. What is the threat posed by non-literal indentation in JSON encoding?

You can’t just point a finger and scream “EMERGENCY” to demand changes. I know that’s how certain politicians work, but we strive to have better standards in technical discussions.

2 Likes

Sorry, but this personal attack disqualifies you from any further replies on my part to you in this thread.

2 Likes

“If you don’t give people a long enough rope to hang themselves, they’ll use a garden hose.”

Fair enough, but the question is valid and still stands. What specific threat does this capability enable? Remember that indent is intended for pretty-printing (it says so in the documentation), so problems with code that reads the output from dump(..., indent=something) are to an extent caused by incorrect use of the argument.

2 Likes

Exactly, LiteralString flags potentially incorrect use, for example by passing unverified user input to it. On the other hand it should be low impact, because (as was pointed out in this thread), it is usually a string literal anyway. (And mypy primer seems to agree.) And in the few cases where it isn’t (for example, because the indention string – not level – is read from a config file), a simple cast to flag this is easy enough.

2 Likes

That’s fine. It’s not my responsibility to prove that your change should be rejected, it’s your responsibility to prove that it should be accepted. Maybe you don’t like the way I made my point, but the point itself still stands, and you declined to answer it.

5 Likes