A practical guide to json-five (pip install json-five, import as json5) — the Python library that parses, round-trips, and emits JSON5.
JSON5 extends JSON with comments, unquoted keys, single-quoted strings, trailing commas, hex numbers, Infinity, NaN, and multi-line strings. The json5 package mirrors the stdlib json API so most code is a drop-in replacement, plus a model API that preserves comments and whitespace on round-trip.
- Why JSON5?
- Installation
- Quick start
- JSON5 syntax at a glance
- Basic API — the
json-compatible interface - Round-trip with comments — the model API
- Editing a model in place
- Custom loaders and dumpers
- Tokenization
- Common recipes
- Gotchas
- Comparison with stdlib
json - Files in this Gist
- Requirements
- References
| Feature | JSON | JSON5 |
|---|---|---|
| Comments | No | // line and /* block */ |
| Unquoted keys | No | foo: "bar" (identifier rules) |
| Single-quoted strings | No | 'bar' equivalent to "bar" |
| Trailing commas | No | {a: 1, b: 2,} is valid |
| Hex integers | No | 0xC0FFEE |
Infinity / NaN |
No | Infinity, NaN |
| Multi-line strings | No | Backslash-continued lines |
JSON5 is useful for configuration files where humans need comments and flexibility, but you still want machine-parseable structure. The json5 Python package lets you read those files and write them back without losing comments — the killer feature that stdlib json cannot do.
pip install json-five # install nameimport json5 # import name — note the mismatch!Requires Python 3.8+. The PyPI package is json-five; the import name is json5.
import json5
# Parse a JSON5 string
text = """{
// app configuration
host: 'localhost',
port: 8080,
debug: true, // trailing comma is fine
}"""
config = json5.loads(text)
print(config)
# {'host': 'localhost', 'port': 8080, 'debug': True}
# Dump back to a JSON5 string
print(json5.dumps(config))
# {host: "localhost", port: 8080, debug: true}{
// Line comment
/* Block comment
spanning multiple lines */
unquoted_key: "value", // identifiers don't need quotes
'single-quoted': "value", // single-quoted keys are fine too
"double-quoted": 'single value', // single-quoted strings work
hex: 0xC0FFEE, // hexadecimal integers
negative: -0xFF,
leading_decimal: .5, // leading zero optional
trailing_decimal: 5., // trailing zero optional
infinity: Infinity,
negative_inf: -Infinity,
nan: NaN,
trailing_comma: true, // trailing commas allowed everywhere
multi_line: "line 1 \
line 2", // backslash line continuation
}The load/loads/dump/dumps functions mirror stdlib json with the same keyword arguments:
import json5
# load / dump — file-based
with open('config.json5') as f:
data = json5.load(f)
with open('out.json5', 'w') as f:
json5.dump(data, f, indent=2)
# loads / dumps — string-based
data = json5.loads('{foo: "bar"}') # {'foo': 'bar'}
text = json5.dumps(data) # {foo: "bar"}| Argument | load/loads |
dump/dumps |
Meaning |
|---|---|---|---|
parse_int |
Yes | — | Callable for integer parsing |
parse_float |
Yes | — | Callable for float parsing |
parse_constant |
Yes | — | Callable for Infinity/NaN/-Infinity |
object_hook |
Yes | — | Called for each object |
object_pairs_hook |
Yes | — | Called with list of pairs |
indent |
— | Yes | Indentation (int or string) |
quote_keys |
— | Yes | Force double-quoted keys (default False) |
trailing_comma |
— | Yes | Emit trailing commas (default False) |
Note: The
clskeyword from stdlibjsonis not supported. For custom serialization, use custom dumpers.
By default, unquoted JSON5 identifiers in object keys become JsonIdentifier objects (a str subclass). This preserves round-trip fidelity — dumps(loads(text)) keeps identifiers unquoted:
>>> text = '{bacon: "eggs"}'
>>> json5.dumps(json5.loads(text))
'{bacon: "eggs"}' # key stays unquoted
>>> json5.dumps(json5.loads(text, parse_json5_identifiers=str))
'{"bacon": "eggs"}' # key now double-quotedUse parse_json5_identifiers=str when you want all keys as plain strings (e.g., for comparison with json-parsed data).
The default loads/dumps pair loses comments because they operate on plain Python objects. To preserve comments, whitespace, and quoting style, use ModelLoader and ModelDumper:
from json5.loader import loads, ModelLoader
from json5.dumper import dumps, ModelDumper
json5_text = """{
// database settings
host: 'localhost',
port: 5432, // default Postgres port
/* SSL only in production */
ssl: false,
}"""
# Load into a model (not plain Python objects)
model = loads(json5_text, loader=ModelLoader())
# Dump the model back — comments and formatting are preserved
roundtripped = dumps(model, dumper=ModelDumper())
print(roundtripped)
# {
# // database settings
# host: 'localhost',
# port: 5432, // default Postgres port
# /* SSL only in production */
# ssl: false,
# }The model is a tree of node objects representing the JSON5 grammar:
>>> model
JSONText(
value=JSONObject(
key_value_pairs=[
KeyValuePair(
key=Identifier(name='host'),
value=SingleQuotedString(characters='localhost', raw_value="'localhost'"),
...
),
...
],
trailing_comma=None,
)
)Each node has .wsc_before and .wsc_after attributes (whitespace and comments) that are preserved through round-trip.
You can modify the model and dump it back — comments on untouched nodes are preserved:
from json5.loader import loads, ModelLoader
from json5.dumper import dumps, ModelDumper
from json5.dumper import modelize
from json5.model import DoubleQuotedString
model = loads("""{
// app config
host: 'localhost',
port: 8080,
}""", loader=ModelLoader())
# Add a new key-value pair to the inner JSONObject
inner = model.value
inner.keys.append(DoubleQuotedString("debug", '"debug"'))
inner.values.append(modelize(True))
print(dumps(model, dumper=ModelDumper()))
# {
# // app config
# host: 'localhost',
# port: 8080,
# "debug":true}Note: The model API does not validate that your edits produce valid JSON5. You are responsible for structural correctness (e.g., comma placement between entries).
A common helper for replacing or adding a top-level key while preserving everything else:
from json5.loader import loads, ModelLoader
from json5.model import DoubleQuotedString, Identifier, JSONText
def set_key(model: JSONText, key_name: str, new_value) -> None:
"""Replace or add a top-level key in a JSONText model."""
json_obj = model.value
new_value_model = modelize(new_value)
for i, key in enumerate(json_obj.keys):
if isinstance(key, Identifier) and key.name == key_name:
json_obj.values[i] = new_value_model
return
if isinstance(key, DoubleQuotedString) and key.characters == key_name:
json_obj.values[i] = new_value_model
return
# Key doesn't exist — append it
json_obj.keys.append(DoubleQuotedString(key_name, f'"{key_name}"'))
json_obj.values.append(new_value_model)Usage:
from json5.dumper import modelize
model = loads('{"foo": "bar", /* preserved */ "baz": "qux"}', loader=ModelLoader())
set_key(model, "foo", "updated")
set_key(model, "new_key", 42)
print(dumps(model, dumper=ModelDumper()))
# {"foo": "updated", /* preserved */ "baz": "qux","new_key":42}Subclass DefaultLoader to customize how specific node types are loaded:
from json5.loader import DefaultLoader, loads
from json5.model import JSONArray
class SingleItemUnwrapper(DefaultLoader):
"""Unwrap single-item arrays to their scalar value."""
def load(self, node):
if isinstance(node, JSONArray) and len(node.values) == 1:
return self.load(node.values[0])
return super().load(node)
text = "{foo: ['bar', 'baz'], bacon: ['eggs']}"
print(loads(text)) # {'foo': ['bar', 'baz'], 'bacon': ['eggs']}
print(loads(text, loader=SingleItemUnwrapper())) # {'foo': ['bar', 'baz'], 'bacon': 'eggs'}Subclass DefaultDumper to customize serialization:
from json5.dumper import DefaultDumper, dumps
class IntBoolDumper(DefaultDumper):
"""Dump booleans as integers (1/0) instead of true/false."""
def dump(self, node):
if isinstance(node, bool):
return self.dump(int(node))
return super().dump(node)
print(dumps([True, False])) # [true, false]
print(dumps([True, False], dumper=IntBoolDumper())) # [1, 0]You can tokenize JSON5 text directly — useful for linters, syntax highlighters, or custom parsers:
from json5.tokenizer import tokenize
for token in tokenize("{foo: 'bar'}"):
print(f"{token.type:25s} {token.value!r}")Output:
LBRACE '{'
NAME 'foo'
COLON ':'
WHITESPACE ' '
SINGLE_QUOTE_STRING "'bar'"
RBRACE '}'
Token types include: LBRACE, RBRACE, LBRACKET, RBRACKET, COLON, COMMA, NAME, WHITESPACE, SINGLE_QUOTE_STRING, DOUBLE_QUOTE_STRING, NUMBER, TRUE, FALSE, NULL, NAN, INFINITY.
from json5.loader import load, ModelLoader
from json5.dumper import dump, ModelDumper
from json5.dumper import modelize
from json5.model import DoubleQuotedString, Identifier
from pathlib import Path
config_path = Path("app.json5")
# Read with comment preservation
model = load(config_path.open(), loader=ModelLoader())
# Update the "port" key
inner = model.value
for i, key in enumerate(inner.keys):
if isinstance(key, (Identifier, DoubleQuotedString)) and (
getattr(key, 'name', None) == 'port' or
getattr(key, 'characters', None) == 'port'
):
inner.values[i] = modelize(9090)
break
# Write back — comments and formatting preserved
with config_path.open("w") as f:
dump(model, f, dumper=ModelDumper())import json5
json5_text = """{
// comment
foo: 'bar',
hex: 0xFF,
}"""
data = json5.loads(json5_text, parse_json5_identifiers=str)
import json
print(json.dumps(data, indent=2))
# {
# "foo": "bar",
# "hex": 255
# }from json5.model import *
from json5.dumper import dumps, ModelDumper
model = JSONText(value=JSONObject(
KeyValuePair(key=Identifier('bacon'), value=Infinity()),
))
print(dumps(model, dumper=ModelDumper()))
# {bacon:Infinity}The ModelDumper and modelize functions produce JSON5 output — single-quoted strings, unquoted identifiers, no trailing commas by default. This is not valid JSON:
>>> from json5.dumper import modelize, dumps, ModelDumper
>>> dumps(modelize({"foo": "bar"}), dumper=ModelDumper())
"{'foo':'bar'}" # single quotes — invalid JSONIf you need strict JSON output, use the default dumps (not ModelDumper) or post-process with a formatter like prettier or biome:
# Correct — uses DefaultDumper, produces valid JSON
>>> json5.dumps({"foo": "bar"})
'{"foo": "bar"}' # wait, this is actually JSON5 with quoted keys
# For strict JSON, use stdlib json
>>> import json
>>> json.dumps({"foo": "bar"})
'{"foo": "bar"}'pip install json-five # install name has a hyphen
import json5 # import name has no hyphenThe default loads returns plain Python objects. Comments, whitespace details, and quoting style are lost. Use ModelLoader + ModelDumper for round-trip with comments.
Unlike stdlib json, you cannot pass cls=MyEncoder. Use custom dumpers instead.
The underlying model node classes (JSONText, JSONObject, DoubleQuotedString, etc.) are subject to breaking changes even in minor releases. Pin your version if you depend on them:
pip install json-five==1.1.2>>> data = json5.loads('{foo: "bar"}')
>>> type(list(data.keys())[0])
<class 'json5.model.JsonIdentifier'>
>>> isinstance(list(data.keys())[0], str)
True # it IS a str subclass
>>> list(data.keys())[0] == "foo"
True # and compares equalMost code won't notice, but type(key) is str checks will fail. Use parse_json5_identifiers=str if you need plain str keys.
| Capability | json |
json5 |
|---|---|---|
| Parse standard JSON | Yes | Yes |
| Parse JSON5 (comments, unquoted keys, etc.) | No | Yes |
load/loads/dump/dumps API |
Yes | Yes (compatible) |
parse_int/parse_float/parse_constant |
Yes | Yes |
object_hook/object_pairs_hook |
Yes | Yes |
cls keyword (custom encoder/decoder) |
Yes | No — use custom loaders/dumpers |
| Comment preservation (round-trip) | No | Yes — via ModelLoader/ModelDumper |
| Tokenization | No | Yes |
| Abstract model API | No | Yes |
Hex integers (0xFF) |
No | Yes |
Infinity / NaN literals |
No | Yes |
| File | Description |
|---|---|
README.md |
This guide — comprehensive json-five usage reference |
explore_basics.py |
Create, read, and write actual JSON and JSON5 files — shows the difference between stdlib json and json5, and demonstrates that default json5.dumps loses comments |
explore_model_api.py |
Full CRUD (create, read, update, add, delete) on a JSON5 file with comments — demonstrates comment preservation through every operation via ModelLoader/ModelDumper |
explore_double_quoted.py |
Full file lifecycle (create → read → update → add → delete) on both .json (valid JSON, stdlib-parseable) and .jsonc (comments preserved) files using the _modelize_json pattern |
AGENTS.md |
Instructions for AI agents maintaining this guide |
CHANGELOG.md |
Version history of this guide |
#json-five-GistSummary |
One-line summary for gist listing |
Each script is a self-contained PEP 723 file — run directly with uv:
uv run --script explore_basics.py
uv run --script explore_model_api.py
uv run --script explore_double_quoted.pyFiles are created in a temp directory and cleaned up at the end — no side effects on your system.
- Python 3.8+
json-five(pip install json-five)