Help with accessing optional part of a self defined option

I defined a custom option that I have a hard time setting a default value on the spot, so I leave the default empty.

Example:

  myCustomList = mkOption {
    type = types.listOf (
      types.submodule {
        options = {
          myOptionalOption = mkOption {
            type = types.str;
            description = "I want to leave this empty when I set the config";
          };
        };
      }
    );
  };

Then I use this function to map over the list

  consumeOption =
    {
      myOptionalOption ? "Set default here",
    }:
    myOptionalOption;

Yet this does not work and gives me back the error “myOptionalOption was accessed but has no value defined. Try setting the option.”

Trying to access the option in other ways all lead to the same error.

However if I do this:

myCustomList = mkOption {
  type = types.listOf (types.attrsOf types.anything);
};

And then I use the same function to consume the configured list, everything works fine, and the destructuring works without error.

I would like to define the options more clearly so I want to use the more detailed way of setting the option, is there something I’m doing wrong?

Can you share the actual code in full, or at least a minimal, one-file reproduction?

1 Like

Here is an example. I am evaluating this using nix repl and a simple flake

{ lib, config, ... }:
let
  consumeFunction =
    {
      dependedOn,
      myOptionalOption ? "${dependedOn} here",
    }:
    myOptionalOption;
in
{
  options = with lib; {
    myCustomList = mkOption {
      type = types.listOf (
        types.submodule {
          options = {
            dependedOn = mkOption {
              type = types.str;
              description = "The other option depends on this";
            };
            myOptionalOption = mkOption {
              type = types.str;
              description = "I want to leave this empty when I make the option";
            };
          };
        }
      );
    };
    myCheck = mkOption { type = types.str; };
  };
  config = {
    myCustomList = [
      { dependedOn = "1"; }
      { dependedOn = "2"; }
    ];

    myCheck = lib.concatStrings (map consumeFunction config.myCustomList);
  };
}

Evaluating myCheck results in

nix-repl> outputs.nixosConfigurations.test.config.myCheck
error:
       … while evaluating the attribute 'value'
         at /nix/store/12azrzlywd7jmsg1q9n4dpqqgb33p2wl-source/lib/modules.nix:1118:7:
         1117|     // {
         1118|       value = addErrorContext "while evaluating the option `${showOption loc}':" value;
             |       ^
         1119|       inherit (res.defsFinal') highestPrio;

       … while evaluating the option `myCheck':

       … while evaluating the attribute 'mergedValue'
         at /nix/store/12azrzlywd7jmsg1q9n4dpqqgb33p2wl-source/lib/modules.nix:1192:5:
         1191|     # Type-check the remaining definitions, and merge them. Or throw if no definitions.
         1192|     mergedValue =
             |     ^
         1193|       if isDefined then

       … while evaluating definitions from `/nix/store/46ak6xqnq7spyv5849l1jhaid0y6sbvm-source/test.nix':

       … while evaluating the option `myCustomList."[definition 1-entry 1]".myOptionalOption':

       (stack trace truncated; use '--show-trace' to show the full, detailed trace)

       error: The option `myCustomList."[definition 1-entry 1]".myOptionalOption' was accessed but has no value defined. Try setting the option.

However if I do this

{ lib, config, ... }:
let
  consumeFunction =
    {
      dependedOn,
      myOptionalOption ? "${dependedOn} here",
    }:
    myOptionalOption;
in
{
  options = with lib; {
    myCustomList = mkOption {
      type = types.listOf (types.attrsOf types.anything);
    };
    myCheck = mkOption { type = types.str; };
  };
  config = {
    myCustomList = [
      { dependedOn = "1"; }
      { dependedOn = "2"; }
    ];

    myCheck = lib.concatStrings (map consumeFunction config.myCustomList);
  };
}

And evaluate it again

nix-repl> :r
Loading flake '.'...
Added 9 variables.
_type, inputs, lastModified, lastModifiedDate, narHash, nixosConfigurations, outPath, outputs, sourceInfo

nix-repl> outputs.nixosConfigurations.test.config.myCheck
"1 here2 here"

In both configs I did not set myOptionalOption, yet the second example can successfully evaluate by using the ? operator. Why does the first example not work?

I am not setting the default value where I define the option because you can see it depends on another attribute in the same attribute set.

Because ? in the Nix language will test if an attrset contains a given attribute, but in the module system DSL, every attribute on the final config attrset corresponding to an option is always present, but it’s lazily defined and possibly throws an error like what you see when it is accessed. Therefore, ? won’t do what you want. (There’s a more complicated way to check if a module system option has any definitions, but you don’t need that. See below for what you should do.)

In this situation, define the default inside a config attrset in the submodule:

  {
    myCustomList = mkOption {
      type = types.listOf (
        types.submodule (
          { config, ... }:
          # Inside this submodule definition, `config` is now bound
          # to this submodule's values. If you also need to refer to
          # an outer `config`, use `let` to give it another name
          # outside of this function.
          {
            options = {
              dependedOn = mkOption {
                type = types.str;
                description = "The other option depends on this";
              };
              myOptionalOption = mkOption {
                type = types.str;
                description = "I want to leave this empty when I make the option";
                defaultText = "\"\${dependedOn} here\"";
              };
            };
            config = {
              myOptionalOption = lib.mkOptionDefault "${config.dependedOn} here";
            };
          }
        )
      );
    };
  }