Usage
Getting the library
default.nix at the repository root is the entry point. It takes system,
sources, pkgs and lib as optional arguments and returns, among the
project's other outputs, lib.module-schema. With
npins -- what the repository uses for its own
inputs -- pin module-schema and import it:
let
sources = import ./npins;
pkgs = import sources.nixpkgs { };
module-schema = (import sources.module-schema { }).lib.module-schema;
in
module-schema.optionsToSchema (pkgs.nixos { }).options.services.peertube
Called with no arguments it uses its own pinned nixpkgs. Pass sources, pkgs
or lib to use yours instead. A sources attrset has to carry a nixpkgs
entry: the library reads lib/ascii-table.nix out of the nixpkgs source tree
to percent-encode the URI fragments it builds for $ref pointers.
The repository is also a flake, at
git+https://git.fediversity.eu/fediversity/module-schema, with the same lib
output. The flake declares no inputs of its own, so inputs.nixpkgs.follows
has no effect; call default.nix with your own sources to override the pin.
Evaluating without a package set
The library needs nixpkgs' lib, not an instantiated package set. Passing
lib avoids import sources.nixpkgs { }, which forces
builtins.currentSystem and therefore fails under a pure flake evaluation:
let
lib = import "${sources.nixpkgs}/lib";
in
(import sources.module-schema { inherit sources lib; }).lib.module-schema
The repository's own flake.nix calls the entry point this way. Note that this
covers the library, not the examples below that call pkgs.nixos --
instantiating a NixOS system needs real nixpkgs.
optionsToSchema
The primary entry point. It takes an attrset of options -- all the options declared for a service, say -- and returns a schema for a configuration of that service:
optionsToSchema ((nixos { }).options.services.peertube.database)
=> {
"$defs" = {
# The schema of a valid Nix attribute name, elided here.
attrName = { };
};
"$schema" = "https://json-schema.org/draft/2020-12/schema";
additionalProperties = false;
properties = {
createLocally = {
default = false;
description = "Configure local PostgreSQL database server for PeerTube.";
type = "boolean";
};
host = {
default = "if config.services.peertube.database.createLocally\nthen \"/run/postgresql\"\nelse null\n";
description = "Database host address or unix socket.";
examples = [ "192.168.15.47" ];
type = "string";
};
name = {
default = "peertube";
description = "Database name.";
type = "string";
};
passwordFile = {
default = null;
description = "Password for PostgreSQL database.";
examples = [ "/run/keys/peertube/password-postgresql" ];
type = [
"null"
"string"
];
};
port = {
default = 5432;
description = "Database host port.";
maximum = 65535;
minimum = 0;
type = "integer";
};
user = {
default = "peertube";
description = "Database user.";
type = "string";
};
};
propertyNames = {
"$ref" = "#/$defs/attrName";
};
type = "object";
}
Several things are worth pointing out in that output:
optionsToSchemais the only function that emits the$schemaroot key, so it is the one to use for a schema that stands on its own as a document.- The result is an object schema. Each option becomes an entry in
properties, every option without a default is listed inrequired, andadditionalProperties = falserejects what the module does not declare.propertyNamesrefers to a$defsentry that describes a valid Nix attribute name. portistypes.port, whose bounds survive asminimumandmaximum.passwordFileisnullOr str, and rather than becoming ananyOfit collapses into a single schema withtype = [ "null" "string" ].hostcarries adefaultText, which replacesdefaultin the schema. Whenever a module author provides one it is the better representation to show, so it wins even when the actual default would serialize fine.
optionToSchema
The same conversion for a single option. It emits no $schema, which makes it
the building block to use when embedding the result in a larger schema:
optionToSchema (lib.mkEnableOption "SERVICE")
=> {
default = false;
description = "Whether to enable SERVICE.";
examples = [ true ];
type = "boolean";
}
optionToSchema ((nixos { }).options.services.peertube.database.port)
=> {
default = 5432;
description = "Database host port.";
maximum = 65535;
minimum = 0;
type = "integer";
}
Note that optionToSchema converts whatever option it is handed. The filtering
of hidden and internal options happens while walking an attrset of options, so a
bare optionToSchema on an internal option still produces a schema.
Serializing
The result is an attrset built so that builtins.toJSON turns it into a JSON
Schema:
builtins.toJSON (optionsToSchema options)
For a schema checked into a repository, writing it out with stable key ordering keeps diffs readable:
pkgs.runCommand "service.schema.json" { nativeBuildInputs = [ pkgs.jq ]; } ''
jq -S . ${pkgs.writers.writeJSON "schema.json" (optionsToSchema options)} > $out
''
Authoring modules that convert well
A few habits make a module convert cleanly:
- Hide what the interface should not see. Options marked
internal = trueorvisible = falseare skipped entirely, as the generated manuals skip them. Use them for implementation detail. - Check the type is supported. An unsupported type throws. Coverage lists what converts. The library is meant to handle any module eventually; until it handles a given type, either filter the options of that type out before conversion or hide them as above.
functionTois not supported yet. Nix'stoJSONandfromJSONcannot usefully round-trip a function, and the library defines no JSON representation for one. Conversion throws with a message saying so, and suggesting to mark the optioninternalorvisible = false.- An option without a
defaultis treated as required. The conversion leans towards requiring options even where they are not strictly required, so that anything validating against the schema can actually be deployed. visible = "shallow"keeps the option but widens its sub-structure to a permissive schema -- useful for a large nested type an interface should not try to render in full.
Seeing it work
Some bigger examples live in the repository itself: lib/tests/services/ holds
schemas for the Mastodon, PeerTube and Pixelfed NixOS modules.
lib/tests/module-schema.nix covers the type matrix option by option, and is
the quickest reference for what a particular lib.types constructor turns into.
Coverage summarizes the same ground in prose.