Skip to content

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.