Worked example: modeling package.json¶
The second of four real-world worked examples, deliberately
different from the pyproject.toml example in one
important way:
package.json has no formal spec at all. npm's
package.json documentation
is explicitly a guide ("This document is all you need to know about
what's required in your package.json file"), not a PEP/RFC-style
standard — there's no numbered specification to cite the way
pyproject.toml has PEP 517/621/639/794. The closest thing to a
checkable reference is a third-party, community-maintained
JSON Schema from SchemaStore
— not an npm/Node.js artifact, and nothing documents how tightly it
tracks npm's actual behavior.
That absence of a formal spec means "full coverage" isn't even a well-defined target here the way it was for pyproject.toml's PEP-enumerated field list. This schema deliberately models a scoped core subset — 16 of the ~30+ fields npm documents — rather than attempting completeness against a moving, informally-documented target.
Source, code, and fixtures:
examples/package-json/.
Schema: package.osd.
Script: convert.py.
Modeled against npm's package.json docs and the creating-a-package-json-file guide, both retrieved 2026-07-11.
Run it¶
python3 examples/package-json/convert.py
read_json is called with schema=, the idiomatic call once the
schema is already in hand — a no-op here too, for the same reason as
the pyproject example: package.osd has no date/time/datetime/
number field either.
Each fixture's OML is also committed alongside it as <name>.oml,
generated by the same write_oml call —
npm-init-default.oml,
invented-widget-cli.oml.
tests/test_examples_package_json.py recomputes each and asserts an
exact match.
The fixtures¶
| File | OML | Arrays OML | What it is | What it exercises |
|---|---|---|---|---|
npm-init-default.json |
.oml |
.arrays.oml |
The official npm init --yes output from npm's own creating-a-package-json-file guide, verbatim |
repository and bugs in object form; author as an empty string; keywords as an empty array |
invented-widget-cli.json |
.oml |
.arrays.oml |
Synthetic, written for this example — not sourced from any real project | author, bugs, and repository in the opposite shapes from the other fixture (object/string/string vs. string/object/object); bin as a multi-command open map; scripts, dependencies, devDependencies, engines |
The .arrays.oml siblings collapse dependencies/devDependencies/keywords-style repeated fields into [...] array syntax. See OML arrays.
No third-party project's own package.json was used, for the same
copyright reason as the pyproject example: everything here is either an
official documentation example explicitly meant to be copied, or clearly
synthetic.
What OSD can model exactly¶
8 of 16 fields — exactly half — are fully closed and checked: name,
version, description, keywords, homepage, license, main,
private.
What it can't, and why¶
The same three structural gaps as pyproject.toml, but concentrated differently — package.json's core fields lean on unions and open maps more heavily, proportionally, despite the schema being smaller:
Unions, on author, bugs, and repository — each can be a plain
string or an object, and repository additionally has several string
shorthand forms ("npm/example", "github:npm/example",
"gitlab:user/repo", "bitbucket:user/repo") beyond the full URL.
bin compounds this: it's a union (string or map) and an open map
in the object case — command name is caller-chosen, not fixed.
Open key sets: scripts (lifecycle-event name → shell command),
dependencies/devDependencies (package name → semver range), and
engines (runtime name → version range) are all genuinely open — the
keys are chosen by whoever writes the file, same as [tool] was for
pyproject.toml.
No real cross-field constraint at this scope — worth noting as a
contrast, not every format has all three gap types. name/version
being "required to publish" isn't enforced by the schema (both are
[0,1], resolved permissively, same principle as pyproject.toml's
build-system.requires — a file legitimately using npm for local
tooling without ever publishing shouldn't be rejected).
The honest number: half of this deliberately-scoped core schema is
any. The full ~30-field format would very likely be a higher
fraction still, since fields left out of scope here (exports,
funding, bundleDependencies) are, if anything, more complex unions
than the ones included.
Comparison with pyproject.toml, as a modeling exercise¶
The interesting finding isn't which format "wins" — it's that the
proportion of unchecked surface tracks the format's own design
discipline, not the schema author's effort. pyproject.toml has PEPs
enumerating exact field shapes; even so, 7 of 20 fields end up any.
package.json has no such process — just years of accreted convention
— and a smaller, hand-scoped core schema still lands at 8 of 16. A
spec-free format's own looseness shows up directly in how much of it a
structural schema language can actually check.
See also¶
This is one of four real-world worked examples — see
the overview for the full comparison table across all four,
plus the any type.