Sigh. Re-release *again* as v0.17.2. I pushed the package and *then* committed the code; it needs to be the other way around.
A Python implementation of the JSON5 data format.
JSON5 extends the JSON data interchange format to make it slightly more usable as a configuration language:
JavaScript-style comments (both single and multi-line) are legal.
Object keys may be unquoted if they are legal ECMAScript identifiers
Objects and arrays may end with trailing commas.
Strings can be single-quoted, and multi-line string literals are allowed.
There are a few other more minor extensions to JSON; see the above page for the full details.
This project implements a reader and writer implementation for Python; where possible, it mirrors the standard Python JSON API package for ease of use.
There is one notable difference from the JSON api: the load() and loads() methods support optionally checking for (and rejecting) duplicate object keys; pass allow_duplicate_keys=False to do so (duplicates are allowed by default).
This is an early release. It has been reasonably well-tested, but it is SLOW. It can be 1000-6000x slower than the C-optimized JSON module, and is 200x slower (or more) than the pure Python JSON module.
Please Note: This library only handles JSON5 documents, it does not allow you to read arbitrary JavaScript. For example, bare integers can be legal object keys in JavaScript, but they aren't in JSON5.
One more note: It's long past time to declare this a 1.0 stable product and move on, so I am preparing to do so. If the latest version proves to be stable (e.g., no bug reports for a month or so), I plan to switch the directory layout to move json5 under a src directory (i.e., use a src-layout to avoid accidentally importing an unpackaged version of the code), bake that for a month or so as well, and then release this as 1.0.
Did I mention that it is SLOW?
The implementation is based on recursive descent parsing and relies on the runtime stack for recursion; this means that really deeply nested data might trigger a RecursionError, but this doesn't seem to be a problem in normal practice. If it is, please report it and I can raise the priority of fixing this properly :).
The implementation follows Python3's json implementation where possible. This means that the encoding method to dump() is ignored, and unicode strings are always returned. There is a slight difference to the strict argument to load()/loads() in order to be JSON5-compliant.
The cls keyword argument that json.load()/json.loads() accepts to specify a custom subclass of JSONDecoder is not and will not be supported, because this implementation uses a completely different approach to parsing strings and doesn't have anything like the JSONDecoder class.
The cls keyword argument that json.dump()/json.dumps() accepts is also not supported, for consistency with json5.load(). The default keyword is supported, though, and might be able to serve as a workaround.
json5 has no runtime dependencies and it is supported on Python version 3.8 or later.
However, in order to develop, test, build and publish the package you need a bunch of extra tools that are only supported on 3.11 or newer; on older versions, you can run the test, but not do anything else.
When changing the code, do not add anything that only runs on 3.9 or later versions, and do not rely on from __future__ import annotations.
Use a 79-character line and prefer single quotes to double; otherwise follow PEP-8 and the normal ruff format coding style.
The easiest thing to do is to install uv and use uv and the //run script to develop things. See ./run --help for the various commands that are supported.
$ brew install uv $ git clone https://github.com/dpranke/pyjson5 $ cd pyjson5 $ source $(./run devenv) # To activate a venv w/ all the needed dev tools.
Install uv via whatever mechanism is appropriate.
You do not have to create a venv to do anything, if you use the run script, but if you want to invoke tools like ruff directly, you can do so as follows:
$ ./run devenv # which really just calls `uv sync`. $ source .venv/bin/activate
$ ./run presubmit
//run supports a number of other commands:
regen will regenerate the parser when the grammar is updated; in order to run this you need glop to be available as described above.
format will format all the code using ruff format for the Python code and pyproject-fmt for the pyproject.toml file; format --check will report whether the code is formatted properly without actually changing anything. It checks for the coding style described above.
checks checks the code for problems without actually executing the code. It runs:
ruff checkpylintmypytests runs all of the tests. Write tests using either doctest or unittest. Aim for 100% line coverage in tests (see next bullet).
coverage runs all of the tests using the coverage tool in order to collect code coverage. You can pass -b/--branch to collect branch coverage. You should target 100% line coverage on any changes, as there shouldn‘t be a reason you can’t do this, and actually ensuring that you execute every line can catch problems in Python that might otherwise be caught earlier in statically-typed languages (though the checks go a long way to accomplishing the same thing).
build builds the package to get it ready to publish
publish tries to publish the package. You must specify --dev or --prod to indicate which PyPI repository you want to publish to; explicitly having to mention one or the other helps ensure that you don't try to publish something by accident. You can run publish --check in order to ensure that the package is built correctly without actually trying to publish it. You can only actually publish the package if you have permission to do so, of course.
presubmit just runs all of the above commands at once (though not regen at the moment, because you might not have glop installed; this'll change when glop is properly packaged and installed in the venv.
Other run commands:
devenv creates a virtual environ using uv and echoes the command needed to activate it.
clean will remove any unversioned files from the repo.
help prints usage.
Update the version in json5/version.py to $VERSION, which should be of the form X.Y.Z, where X, Y, and Z are numbers. Use semantic versioning when increasing the numbers:
In between releases, the version may be set to `X.Y.Z.dev0' to indicate that this is not an officially released version.
./run presubmit once more and make sure everything passes.
git commit -a -m "Bump the version to $VERSION"
git tag "v$VERSION"
git push origin
git push --tags origin
./run publish --prod (which will build the package if need be first)
(Assuming you have upload privileges to PyPI and the GitHub repo, of course.)
v0.17.2 (2026-10-09)
v0.17.1 (2026-10-09)
v0.17.0 (2026-10-09)
glop, rather than relying on it being checked out alongside this repo. This should make the package properly self-contained, and now we can enforce that the grammar is up-to-date in presubmit.dev venv config will only install the dev packages in that newer environment; this should make Dependabot happier.//run script works; the previous pylint and mypy checks have been moved into the checks command, which will now run all of the static analyzers, not just ruff check. The script also now consistently supports -f/--failfast across the commands, not just during tests.v0.16.0 (2026-10-07)
RecursionError issue.strict=False option to allow them. In JSON5, they are allowed (except for \r and \n). json5 also has the strict option, and was initially supposed to match json here (I think), but due to a bug only \r and \n were being disallowed. However, this behavior is actually correct (they should always be allowed), and now strict=False will just control whether \r and \n will be allowed. This isn‘t a behavior change, but it might change what you expected the behavior to be. I’ve updated the docs.start wasn't being handled properly in the load() call.load() and `loads() to reflect that either bytes or strings might be passed in.ruff lint and pylint.v0.15.0 (2026-06-19)
-0x1f and +0xff was raising a ValueError instead of being allowed.v0.14.0 (2026-03-27) This is really just a dependency bump release.
v0.13.0 (2026-01-01)
v0.12.1 (2025-08-12)
v0.12.0 (2025-04-03)
v0.11.0 (2025-04-01)
cls argument; turns out mypy was right and I was wrong and I didn't realize it :).parse method that can be used to iterate through a string, extracting multiple values.consume_trailing parameter to load/loads/parse that specifies whether to keep parsing after a valid object is reached. By default, this is True and the string must only contain trailing whitespace. If set to False, parsing will stop when a valid object is reached.start parameter to load/loads/parse to specify the zero-based offset to start parsing the string or file from.v0.10.0 (2024-11-25)
JSON5Encoder class that can be overridden to do custom encoding of values. This class is vaguely similar to the JSONEncoder class in the standard json library, except that it has an encode() method that can be overridden to customize any value, not just ones the standard encoder doesn't know how to handle. It does also support a default() method that can be used to encode things not normally encodable, like the JSONEncoder class. It does not support an iterencode method. One could probably be added in the future, although exactly how that would work and interact with encode is a little unclear.quote_style argument to dump()/dumps() to control how strings are encoded by default. For compatibility with older versions of the json5 library and the standard json library, it uses QuoteStyle.ALWAYS_DOUBLE which encodes all strings with double quotes all the time. You can also configure it to use single quotes all the time (ALWAYS_SINGLE), and to switch between single and double when doing so eliminates a need to escape quotes (PREFER_SINGLE and PREFER_DOUBLE). This also adds a --quote-style argument to python -m json5.v0.9.28 (2024-11-11)
uv so ./run tests works properly.v0.9.27 (2024-11-10)
v0.9.26 (2024-11-10)
strict parameter to load()/loads().run script to be contemporary.v0.9.25 (2024-04-12)
v0.9.24 (2024-03-16)
hypothesize in v0.9.23.v0.9.23 (2024-03-16)
hypothesize. This ran model-based checks and didn‘t really add anything useful in terms of coverage to the test suite, and it introduced dependencies and slowed down the tests significantly. It was a good experiment but I think we’re better off without it.ruff check using ruff 0.3.3 (Note that commit message in 00d73a3 says pylint 3.11, which is a typo).ruff formatv0.9.22 (2024-03-06)
v0.9.21 (2024-03-06)
v0.9.20 (2024-03-03)
json5.__version__ in addition to json5.VERSION.//run script to use python3.v0.9.19 (2024-03-03)
v0.9.18 (2024-02-29)
v0.9.17 (2024-02-19)
setup.py to pyproject.toml.v0.9.16 (2024-02-19)
setup.py//README.md.v0.9.15 (2024-02-19)
v0.9.14 (2023-05-14)
+Infinity as well as -Infinity and Infinity.v0.9.13 (2023-03-16)
v0.9.12 (2023-01-02)
v0.9.11 (2023-01-02)
float("inf") instead of math.inf.v0.9.10 (2022-08-18)
v0.9.9 (2022-08-01)
int or float: Previously we would use the objects str implementation, but that might result in an illegal JSON5 value if the object had customized str to return something illegal. Instead, we follow the lead of the JSON module and call int.__repr__ or float.__repr__ directly.allow_nan=False.v0.9.8 (2022-05-08)
v0.9.7 (2022-05-06)
default fn in dump and dumps. Previously we didn‘t require the function to return a string, and so we could end up returning something that wasn’t actually valid. This change now matches the behavior in the json module. Note: This is a potentially breaking change.v0.9.6 (2021-06-21)
hypothesis isn't installed.v0.9.5 (2020-05-26)
v0.9.4 (2020-03-26)
v0.9.3 (2020-03-17)
v0.9.2 (2020-03-02)
v0.9.1 (2020-02-09)
v0.9.0 (2020-01-30)
dump()/dumps(). Add an allow_duplicate_keys=False to prevent possible ill-formed JSON that might result.v0.8.5 (2019-07-04)
v0.8.4 (2019-06-11)
v0.8.3 (2019-06-11)
v0.8.2 (2019-06-11)
v0.8.1 (2019-06-11)
v0.8.0 (2019-06-11)
allow_duplicate_keys=True as a default argument to json5.load()/json5.loads(). If you set the key to False, duplicate keys in a single dict will be rejected. The default is set to True for compatibility with json.load(), earlier versions of json5, and because it's simply not clear if people would want duplicate checking enabled by default.v0.7 (2019-03-31)
quote_keys=True will turn that off and always quote object keys.indent is not None). Passing trailing_commas=False will turn that off.json5.tool command line tool now supports the --indent, --[no-]quote-keys, and --[no-]trailing-commas flags to allow for more control over the output, in addition to the existing --as-json flag.json5.tool command line tool no longer supports reading from multiple files, you can now only read from a single file or from standard input.json module for anything. The output should still match the json module (except as noted above) and discrepancies should be reported as bugs.v0.6.2 (2019-03-08)
v0.6.1 (2018-05-22)
v0.6.0 (2017-11-28)
v0.5.0 (2017-09-04)
json module supports.