Coverage

What the conversion expresses today, and what it refuses.

A type that is not supported throws Schema conversion not yet implemented for <name>. The issue tracker lists which types those are. Some lib.types constructors build the same type and so convert the same way: port and ints.u16, uniq and unique.

Types that convert

lib.types Emitted as
bool type = "boolean"
enum enum with the listed values
str, lines, commas, envVar, separatedString type = "string"
strMatching <regex> type = "string"
passwdEntry <type> type = "string" with pattern = "^[^:\n]*$", the check that rejects colons and newlines
path, pathWith, pathInStore type = "string"
package, shellPackage $ref to the package object in $defs, see Packages
int type = "integer" with the 64-bit minimum/maximum
float, number type = "number" with double-precision bounds
ints.between, numbers.between the numeric type with minimum/maximum parsed out of the type description
ints.positive type = "integer" with exclusiveMinimum = 0
port, ints.u16 type = "integer", minimum = 0, maximum = 65535
attrs type = "object", propertyNames restricted to valid Nix attribute names, additionalProperties the permissive value schema
attrsOf, lazyAttrsOf, attrsWith type = "object" with the element schema as additionalProperties
listOf type = "array" with the element schema as items
nonEmptyListOf the listOf schema with minItems = 1, and no default, as the empty list the type rejects
nullOr null merged into the element's type where the element has one, an anyOf otherwise
either, oneOf one merged schema where the alternatives merge (either int str becomes type = [ "integer" "string" ]), an anyOf otherwise; oneOf folds pairwise into either
submodule, submoduleWith type = "object" with properties, inferred required, and additionalProperties = false unless the module has a freeform type
attrTag type = "object" with a oneOf of single-tag branches. The uninhabited attrTag { } becomes { type = "object"; not = { }; }, since oneOf = [ ] is not a legal schema
deferredModule the attrs schema
unspecified, raw, anything the permissive value schema, pulled in by $ref
pkgs.formats.{json,yaml,toml,ini} types the format's recursive value type, resolved through $ref and $defs

Packages

A user names a package by its attrpath in Nixpkgs, though the real type is a derivation. package refers to a shared definition in $defs:

{
  "$ref": "#/$defs/package",
  "$defs": {
    "package": {
      "type": "object",
      "properties": {
        "_type": { "const": "package" },
        "attrpath": { "type": "array", "items": { "type": "string" }, "minItems": 1 }
      },
      "required": ["_type", "attrpath"],
      "additionalProperties": false
    }
  }
}

{ "_type": "package", "attrpath": ["python3Packages", "requests"] } refers to pkgs.python3Packages.requests. Nixpkgs has a pkgs attribute, so ["pkgs", "python3Packages", "requests"] refers to the same package.

An object keeps a package distinct from a string, so either package str becomes an anyOf of the two and a value shows which one it is. The _type marker follows the Nix convention for tagged values such as mkIf. It keeps a package distinct from other objects, such as the value of a submodule with an attrpath option, so a consumer can find packages in the data without the schema.

resolvePackages converts such data back to Nix. It replaces each package value with the package it refers to in a package set:

config = module-schema.resolvePackages pkgs (builtins.fromJSON json);

A default or example that the option documents as Nix code, such as the pkgs.hello that mkPackageOption renders, stays a string of Nix code, as in lib.optionAttrSetToDocList.

functionTo

functionTo throws. Nix's toJSON and fromJSON cannot usefully round-trip a function, and the library defines no JSON representation for one yet. The message says so, and suggests marking the option internal or visible = false so it is excluded from the interface.

Module system features

  • description, default, example (rendered as examples, with a literalExpression unwrapped to its text, and de-duplicated against the type's empty value)
  • type.emptyValue, used as a default when the option sets none
  • defaultText, which overrides default with its rendered .text, matching what lib.optionAttrSetToDocList does for the manual
  • readOnly, passed through as it is. Nix means "can be set only once" by it, which is close to, but not the same as, the JSON Schema meaning
  • internal = true and visible = false, dropped
  • visible = "shallow" and visible = "transparent", both of which keep the option. "shallow" replaces the sub-structure with the permissive value schema rather than dropping it, so the schema still accepts everything the option accepts
  • freeform modules: _module.freeformType becomes additionalProperties, propagated one nesting level down
  • required inference for submodules and for nested attrsets of options, deliberately over-approximated so that anything validating against the schema is deployable
  • _module and _freeformOptions stripping, so a schema built from a raw getSubOptions does not leak them as properties
  • type-level recursion, resolved with $ref and $defs

Note that the internal/visible filtering happens while walking an attrset of options. A bare optionToSchema applied to an internal option still produces a schema.

JSON Schema dialect

The output is draft 2020-12.

Where this is checked

  • lib/tests/module-schema.nix -- the type matrix and the visibility rules.
  • lib/tests/internal.nix -- the schema algebra: allowsType, subsumes, union. One case there asserts the deliberately wrong answer allowsType gives for a schema whose types are constrained by an anyOf, so the wrong answer is pinned rather than accidental.
  • lib/tests/interval.nix -- the interval arithmetic behind the numeric bounds.
  • lib/tests/services/ -- golden schemas for the Mastodon, PeerTube and Pixelfed NixOS modules.