- Nix 98.5%
- Python 1.5%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
The sidecar hands home-manager the JSON through toJSON, so nothing runs the validator over it unless the consumer builds validate as a check; the README said the file could not be invalid. The one place the types are stricter than the schema, an inferred object or array, and the depth at which a recursive property documents were missing from the same guarantees. The order of work is gone from the design, since every part of it is in the tree. Assisted-by: Claude Code:Fable 5.1 |
||
| docs | ||
| example | ||
| lib | ||
| tests | ||
| flake.lock | ||
| flake.nix | ||
| README.md | ||
jsonschema2nix
Turn a JSON Schema into NixOS module options. A settings-style option typed
from a schema gets completion, documentation, merging across modules and
early type errors, and the file written from it is validated against the
original schema, so an invalid configuration cannot build.
Nothing is generated into your repository: the schema is read at evaluation time. The one exception is a lock file for schemas addressed by URL.
Usage
A schema in the repository, with or without relative $refs to sibling
files:
{ lib, config, options, pkgs, ... }:
let
schema = jsonschema2nix.lib.fromFile ./schema.json;
in
{
options.services.foo.settings = lib.mkOption { type = schema.type; };
config.services.foo.configFile = schema.generate pkgs {
name = "foo.json";
option = options.services.foo.settings;
};
}
generate takes the option, not its value: only what the configuration sets
is written. A schema default becomes the option's default, so it is what
config reads and what the documentation shows, but it is not written to the
file, because it is the tool's own default and writing it would freeze it at
the schema's revision. Pass check = false to skip validation.
A schema addressed by URL. The lock records the URL, every document it references, their hashes, and where to fetch them from; re-run the same command to update it. Schema URLs are not versioned, so where a host also serves the same bytes at a revision-pinned URL the lock points at that.
nix run github:natsukium/jsonschema2nix#lock -- https://json.schemastore.org/pyproject.json > pyproject.lock.json
schema = jsonschema2nix.lib.fromLock ./pyproject.lock.json;
The record returned by fromFile and fromLock follows the shape of
pkgs.formats:
type |
the root type |
toJSON <option> |
the JSON value the option's definitions wrote |
generate pkgs { name; option; check ? true; } |
the validated file |
validate pkgs { name; value; } |
validate a value headed for another format |
override { overlays; types; } |
hints keyed by JSON pointer, see below |
report |
what the types leave to the validator, by pointer |
lib needs only nixpkgs' lib; pkgs is taken where a derivation is built.
nixpkgs 25.11 or later is required.
Existing options
home-manager's programs.starship.settings and the like are declared with a
bare format type, which nothing can extend. The typed option sits beside it
and feeds it:
options.programs.starship.typedSettings = lib.mkOption { type = schema.type; };
config.programs.starship.settings = schema.toJSON options.programs.starship.typedSettings;
home-manager then writes the file as it always does, presets and all. What
reaches the file is typed but not validated, because home-manager writes it,
not generate; to keep the guarantee, build validate over the same value
as a check:
checks.starship-settings = schema.validate pkgs {
name = "starship.json";
value = config.programs.starship.settings;
};
What the types check, and what they leave to the validator
The types carry what the module system can express: object shapes, required
keys, enums, unions, numeric and length bounds, ECMAScript patterns that
POSIX ERE can hold. Everything else (format, if/then/else, not,
patterns with lookaround, a fractional multipleOf, ...) is checked by the
validator when the file is built. schema.report lists, by JSON pointer,
what a given schema leaves to that step. The types never reject a value the
schema accepts, with one deliberate exception: a subschema that writes
properties or items without a type is read as an object or an array,
which the report names as an inferred type.
Where a schema cannot be typed as written, override merges subschema
fragments into it before typing, or injects a lib.types value at a pointer.
The validator still checks against the untouched upstream schema, so a wrong
hint is caught rather than hidden.
Editor completion
nixd descends into a submodule option by calling getSubOptions on its type,
so pointing it at the evaluated options of a configuration gives completion,
hover and defaults for every schema property:
{
"nixd": {
"options": {
"home-manager": {
"expr": "(builtins.getFlake \"/path/to/config\").homeConfigurations.\"user\".options"
}
}
}
}
It does not descend into a union or a nullable object; that is a nixd limitation shared by every module. A property whose type refers back to its own definition documents one level deep, so completion stops there too.
Design
docs/design.md: the four stages, the rules each applies, what the schemas it was run against exposed, and unsupported features grouped by how much work they are.