Skip to content

NixOS: default = {} for submodule not respected #112494

@fricklerhandwerk

Description

@fricklerhandwerk

Describe the bug
Setting a submodule default value to {} does not actually result in an empty set. Setting no default value leads to the option being not defined, but using default = {}; fills the set with attributes declared in its options.

To Reproduce
Minimal example configuration:

# config.nix
{ ... }:
let
  extension = { lib, ... }: {
    options = with lib; with types; {
      users.users = mkOption {
        type = attrsOf (submodule {
          options.foo = mkOption {
            description = "a foo extension for users";
            default = {};
            type = submodule {
              options = { bar = mkOption { type = str; }; };
            };
          };
        });
      };
    };
  };
in
{
  imports = [ extension ];

  users.users = {
    extended = {
      foo = { bar = "baz"; };
    };
    vanilla = {};
  };
}
$ nix repl -E 'import <nixpkgs/nixos> { configuration = ./config.nix; }'
nix-repl> config.users.users.extended.foo
{ bar = "baz"; }
nix-repl> config.users.users.vanilla.foo
{ bar = «error: The option `users.users.vanilla.foo.bar' is used but not defined.»; }

Expected behavior

$ nix repl -E 'import <nixpkgs/nixos> { configuration = ./config.nix; }'
nix-repl> config.users.users.extended.foo
{ bar = "baz"; }
nix-repl> config.users.users.vanilla.foo
{ }

Additional context

My use case is to extend users.users.<name> with a new submodule option (here foo), which should only be acted upon if a value is actually set. Following the NixOS manual there is no indication that default = {} will still populate the set. Not setting default does not help either, because there is no (obvious) way to check if a value has been set, since the not defined exception will trigger on evaluation.

Sure, the workaround is to just wrap the submodule in a nullOr type and default = null

mkOption {
  description = "a foo extension for users";
  default = null;
  type = nullOr (submodule {
    options = { bar = mkOption { type = str; }; };
  });
}

and check for null in the implementation.

This should at least be documented (which it is now, here...), but I'd much prefer that the behavior of default = {} was less surprising. I'd submit a PR but I'd need some hand holding to dig through /lib/modules.nix, because so far I could not find how exactly it comes that the default empty set is not the final value.

Notify maintainers
@infinisil @danbst

Metadata

Metadata

Assignees

No one assigned

    Labels

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions