How can I configure default values (lib.mkDefault) for options in a submodule option?

For context, I am using nix-minecraft to configure a Minecraft server on my system. I would like to configure some more secure defaults, as Minecraft has some relatively insecure settings by default. The services.minecraft-servers.servers option is defined as a types.attrsOf (types.submodule {...}) type. I’ve tried the following configuration, which was intended to get a list of defined servers from the services.minecraft-servers.servers option and use it to generate defaults for any server that is defined.

{ pkgs, lib, config, ... }:
{
  config = lib.mkMerge [
    (lib.mkIf config.services.minecraft-servers.enable {
      services.minecraft-servers.servers = lib.genAttrs (builtins.attrNames config.services.minecraft-servers.servers)
        (_: {
          serverProperties = {
          # Use whitelist.
          white-list = lib.mkDefault true;
          enforce-whitelist = lib.mkDefault true;

          # Disables chat reporting requirement; does nothing to aid in
          # security and only serves to make the server less private.
          enforce-secure-profile = lib.mkDefault false;

          # Require online authentication for users.
          online-mode = lib.mkDefault true;
        };
      });
    })
  ];
}

What I was intending to do with the above is to configure my system so that any server defined in services.minecraft-servers.servers would have the default options defined above in serverProperties. However, this just results in an infinite recursion:

warning: Git tree '/home/nullbite/nixfiles' is dirty
building the system configuration...
warning: Git tree '/home/nullbite/nixfiles' is dirty
error:
       … while calling the 'head' builtin

         at /nix/store/cp5s6lac68bzypgc1chl8c0ss3rqxi27-source/lib/attrsets.nix:1541:11:

         1540|         || pred here (elemAt values 1) (head values) then
         1541|           head values
             |           ^
         1542|         else

       … while evaluating the attribute 'value'

         at /nix/store/cp5s6lac68bzypgc1chl8c0ss3rqxi27-source/lib/modules.nix:809:9:

          808|     in warnDeprecation opt //
          809|       { value = builtins.addErrorContext "while evaluating the option `${showOption loc}':" value;
             |         ^
          810|         inherit (res.defsFinal') highestPrio;

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

       error: infinite recursion encountered

       at /nix/store/cp5s6lac68bzypgc1chl8c0ss3rqxi27-source/lib/modules.nix:809:9:

          808|     in warnDeprecation opt //
          809|       { value = builtins.addErrorContext "while evaluating the option `${showOption loc}':" value;
             |         ^
          810|         inherit (res.defsFinal') highestPrio;

Is there any other way I could set default values for submodule options? I can’t think of any way to access the list of defined options to be able to set them without triggering an infinite recursion.

For reference, the option type is defined here.

I’ve come up with alternative solution for this specific use case that works for what I want. I’ve defined a function mkServer which returns a lib.mkMerge which merges a set of defaults defined in the function with the provided attrset. Here’s a basic example of what I did (my full implementation is here):

mkServer = { ... }@opts: lib.mkMerge [
  # default options
  {
    whitelist = lib.mkDefault {
      # whitelist
    };
    serverProperties = {
      # allows no chat reports to run
      enforce-secure-profile = lib.mkDefault false;

      # whitelist
      white-list = lib.mkDefault true;
      enforce-whitelist = lib.mkDefault true;

      # no telemetry
      snooper-enabled = lib.mkDefault false;
    };
  }
  # argument options
  opts
];

This works for this specific use case, but I’d still like to know if there’s a way to configure submodule option defaults, or more likely, if it’s something I should probably never do. There’s a few other scenarios where I feel like this could be useful (e.g., enabling compression on all btrfs mounts), although there’s probably much better and more granular alternatives, such as custom options or a custom function like above.

While researching something else, I’ve discovered that submodules can be extended which has led to a cleaner solution for my purposes. The following module adds a useRecommendedDefaults option to the server config, which achieves the same as the “mkServer” function, but in a much cleaner and more semantically correct way, which also properly integrates into the NixOS option system (as before, the full implementation is here):

{ config, lib, pkgs, inputs, ... }:
{
  options.services.minecraft-servers.servers = let
    serverModule = { name, config, ... }: {
      options = {
        useRecommendedDefaults = lib.mkOption {
          type = lib.types.bool;
          description = "Whether to use recommended server settings.";
          default = false;
        };
      };

      config = lib.mkIf config.useRecommendedDefaults {
        autoStart = lib.mkDefault true;
        jvmOpts = "-Dlog4j2.formatMsgNoLookups=true";

        whitelist = lib.mkDefault {
          # whitelist
        };

        serverProperties = {
          # allows no chat reports to run
          enforce-secure-profile = lib.mkDefault false;

          # whitelist
          white-list = lib.mkDefault true;
          enforce-whitelist = lib.mkDefault true;

          # this helps with some mod support. disable it on public servers.
          allow-flight = lib.mkDefault true;

          # no telemetry
          snooper-enabled = lib.mkDefault false;
        };
      };
    };
  in lib.mkOption {
    type = with lib.types; attrsOf (submodule serverModule);
  };
}

If you wanted to apply this to every entry by default, you could probably change the default value of useRecommendedDefaults to true, which would still allow it to be disabled. This would effectively solve the problem in my original question, while still allowing it to be configurable.

This gist has another example of this process.