JPML / .jp

Configuration language

JPML

TOML’s sections. JSON’s nesting. Neither one’s ceremony.

.jp files use [SECTION] headers at the top level and {…} / […] structures inside them. Keys need no quotes, # starts a comment, trailing commas are fine, and a value is allowed to be empty.

No runtime dependencies Python 3.14+ · Node 20+ MIT licensed

servers.jp
# Keys before the first header belong to the document root.
version: 2

[SERVER_ID]
config: {
  disabled_channels:,            # empty, on purpose
  disabled_users: [9892, 82082, 8209]
}

[SERVER_ID_2]
prefix: "!"
modules: {
  moderation: true,
  fun: false
}

Why another formatBecause config files are read by people

JSON has no comments, demands quotes on every key, and rejects a trailing comma. TOML has comments and headers, but nesting anything non-trivial means either deeply dotted keys or a table per level. .jp takes the half of each that suits a file a human keeps editing.

Sections on top

A [SERVER_ID] header reads better than another brace, and headers may be dotted to nest or quoted when a name contains a dot.

JSON below it

Inside a section it is objects and arrays, as deep as you like, with the shape you already know from every API you have ever touched.

No ceremony

Unquoted keys, comments anywhere including inside objects and arrays, and trailing or repeated commas quietly ignored.

Empty is a value

disabled_channels:, means the key exists and has nothing in it yet — a real state in a config, and one JSON can only spell as null.

It is a small, fully specified format with a strict parser, precise error messages and a deterministic writer, so files stay stable when a program rewrites them.

The formatEverything there is to learn

There is not much of it. Four ideas and a table of value types cover the whole language.

Sections

A [NAME] header opens a root key. Everything below it, until the next header, belongs to that section.

Headers may be dotted to nest, and quoted when a name contains a dot. Key/value pairs written before the first header land at the document root.

sections
[SERVER_ID]
prefix: "!"

[guild.limits]        # -> {"guild": {"limits": {…}}}
["weird.name"]        # -> {"weird.name": {…}}

Entries

An entry is key: value. Keys need no quotes; a bare key may contain spaces but not brackets, commas or quotes — quote it if it needs those.

Entries are separated by a line break, a comma, or both. Trailing and repeated commas are accepted.

entries
[SERVER_ID]
a: 1
b: {x: 1, y: 2,}
c: [1, 2, 3,]
"key: with punctuation": true

Empty values

A key with nothing after the colon parses to None / null.

Because of that, a value must start on the same line as its :. An opening { or [ goes on the colon’s line; its contents may then wrap freely.

empty values
config: {
  disabled_channels:,      # -> null
  timeout:                 # -> null
}

# An array element cannot be empty, so it says so:
slots: [null, 3]

Comments

# runs to the end of the line and is allowed anywhere, including inside objects and arrays.

They are the one thing a rewrite cannot keep: the writer works from parsed data, and comments are not part of it.

comments
# Template config -- copy to servers.jp and fill in.
[1234567890]
limits: {
  # Nested objects may go as deep as you need.
  rate: {per_minute: 30, burst: 5},
  warn_threshold: 3
}

Values

Type Examples
String "hello" 'hello' hello world
Integer 42 -7 1_000 0xff 0o755 0b1010
Float 3.5 1e3 inf -inf nan
Boolean true false — case-insensitive, so True works too
Null null none nil, or nothing at all
Object {a: 1, b: 2}
Array [1, 2, 3]

Unquoted values are read as a keyword first, then a number, then a plain string. Quote a value if it contains a #, a comma, a bracket, or leading and trailing whitespace you want to keep. Strings honour the usual escapes — \n, \t, \\, \", \uXXXX, \U0001F600, and \ at end of line to continue onto the next.

PackagesTwo parsers that agree

The same format, the same error messages and the same writer, so a file written by one is byte-identical to a file written by the other.

Python jpml

Python 3.14+, typed, no runtime dependencies.

pip install jpml

JavaScript jpml-lang

ESM and TypeScript types, with a /core entry point for the browser.

npm install jpml-lang

Editor jpml-lang.jpml

Highlighting, live errors, outline, folding and Format Document for VS Code.

code --install-extension jpml-lang.jpml
Python
import jpml

data = jpml.load("data/servers.jp")
jpml.dump(data, "data/servers.jp")

# A whole folder, one key per file.
config = jpml.load_dir("data")

# Or edit in place, and save atomically.
from jpml import JPConfig

cfg = JPConfig.load("data/servers.jp", missing_ok=True)
cfg.set_path("SERVER_ID.config.disabled_users", [9892])
cfg.save()
TypeScript
import { load, dump, loadDir, JPConfig } from "jpml-lang";

const data = await load("data/servers.jp");
await dump(data, "data/servers.jp");

// A whole folder, one key per file.
const config = await loadDir("data");

// Or edit in place, and save atomically.
const cfg = await JPConfig.load("data/servers.jp", {
  missingOk: true,
});
cfg.setPath("SERVER_ID.config.disabled_users", [9892]);
await cfg.save();

Writes are atomic by default: the file goes to a temporary neighbour and is renamed into place, so a crash or a concurrent reader never sees half a config.

When it goes wrongAn error that points at the character

Every error derives from JPError. A decode error carries .line, .col, .pos, .filename and .raw_message, so you can render the failure yourself — or just print it.

the message
data/servers.jp:2:8: expected ':' after key 'prefix', found '"'
    prefix "!"
           ^

JPDecodeError is a ValueError and JPEncodeError a TypeError, so existing except clauses keep working. The encode error explains what could not be serialised: an unsupported type, a non-string key, a circular reference.

A repeated key is an error by default rather than a silent overwrite. Pass duplicate_keys="first" or "last" if you would rather it not be.

The VS Code extension runs the same parser as your program, so the editor and the runtime always agree about what is valid.

Command lineWork from the shell

Both packages install a jpml command. python -m jpml … works identically, and - reads standard input.

Validate

jpml check data/*.jp
Non-zero exit on the first failure. Worth a line in CI.
jpml check -q
Errors only, nothing on success.

Format

jpml fmt -w servers.jp
Rewrite in place, in the canonical style.
--indent --width --sort-keys
Two spaces, an 88-column budget for inline arrays, insertion order.

Read and convert

jpml get servers.jp SERVER_ID.prefix
One value by dotted path. -r prints strings unquoted.
jpml to-json jpml from-json
Both directions, to a file with -o or to stdout.

Round tripsA rewritten file is a stable file

dumps is deterministic, so a file rewritten twice is byte-identical. That is what makes it safe for a program to own a config a person also edits.

Two things do not survive a rewrite. Comments are dropped, because they are not part of the parsed data — which is why Format Document in VS Code refuses a file that has any, until you tell it otherwise. And root-level scalars move above the first section, since anything after a header would be read back as part of that section.

In practiceOrganising your configs

Nothing is enforced, but this is the layout load_dir is built for, and a few habits that save pain later.

your-project/
data/
├─ servers.jp            # one file per concern
├─ roles.jp
├─ servers.example.jp    # committed template
└─ guilds/               # one file per entity
   ├─ 1234567890.jp
   └─ 9876543210.jp

One file per concern. A parse error then takes out one feature, not everything.

Keep live data out of git and commit a template instead: data/*.jp ignored, !data/*.example.jp kept.

Use IDs as section names. [1234567890] parses to the string key "1234567890", and integer keys are stringified on write, so the shape round-trips.

Write through save() rather than by hand, so an interrupted write cannot truncate a live config — and validate in CI with jpml check.