From 06e27c84de5efb56ce2310af2010d7d0d978ebda Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Wed, 16 Apr 2025 09:13:19 +0200 Subject: [PATCH] lib/jsonschema: make `attrs` required Before the change, modules of the form ```nix { lib, ... }: { foo.bar = lib.mkOption { # ... }; } ``` or ```nix { lib, ... }: { foo = lib.mkOption { type = lib.types.subModule { bar = lib.mkOption { # ... }; }; }; } ``` would not render with `foo` as required, which is not faithful to the module system's semantics. This change also tests that fields with defaults are not marked required. Note that submodule options cannot have their defaults rendered to JSON schema, and are therefore always marked required. Architecturally this change is rather unfortunate: So far the checks for defaults happen in the rendering (using `isDefault`) and not in the parsing, but here we're adding a field to `$exportedModuleInfo`. While strictly speaking we probably don't want to consider requiredness as module-level information, it seems more reasonable to me to do it that way since at the JSON schema level we have lost the distinction between `attrs`, `attrsOf`, `submodule`. --- lib/jsonschema/default.nix | 14 ++++++++++---- lib/jsonschema/example-schema.json | 1 + lib/jsonschema/test_parseOptions.nix | 23 ++++++++++++++++++++++- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/lib/jsonschema/default.nix b/lib/jsonschema/default.nix index 177fef8d5..855739897 100644 --- a/lib/jsonschema/default.nix +++ b/lib/jsonschema/default.nix @@ -123,7 +123,7 @@ rec { # parse options to jsonschema properties properties = lib.mapAttrs (_name: option: (parseOption' (path ++ [ _name ]) option)) options'; # TODO: figure out how to handle if prop.anyOf is used - isRequired = prop: !(prop ? default || prop.type or null == "object"); + isRequired = prop: !(prop ? default || prop."$exportedModuleInfo".required or false); requiredProps = lib.filterAttrs (_: prop: isRequired prop) properties; required = lib.optionalAttrs (requiredProps != { }) { required = lib.attrNames requiredProps; }; header' = if addHeader then header else { }; @@ -150,9 +150,9 @@ rec { { }; # Metadata about the module that is made available to the schema via '$propagatedModuleInfo' - exportedModuleInfo = lib.optionalAttrs true (makeModuleInfo { + exportedModuleInfo = makeModuleInfo { inherit path; - }); + }; in # return jsonschema header' @@ -377,7 +377,13 @@ rec { # return jsonschema property definition for attrs then default - // exposedModuleInfo + // (lib.recursiveUpdate exposedModuleInfo ( + lib.optionalAttrs (!default ? default) { + "$exportedModuleInfo" = { + required = true; + }; + } + )) // example // description // { diff --git a/lib/jsonschema/example-schema.json b/lib/jsonschema/example-schema.json index 3944cca62..ca2175472 100644 --- a/lib/jsonschema/example-schema.json +++ b/lib/jsonschema/example-schema.json @@ -3,6 +3,7 @@ "$exportedModuleInfo": { "path": [] }, "type": "object", "additionalProperties": false, + "required": [ "services" ], "properties": { "name": { "$exportedModuleInfo": { "path": ["name"] }, diff --git a/lib/jsonschema/test_parseOptions.nix b/lib/jsonschema/test_parseOptions.nix index 137bf52a1..0c2ff8a93 100644 --- a/lib/jsonschema/test_parseOptions.nix +++ b/lib/jsonschema/test_parseOptions.nix @@ -13,7 +13,17 @@ testParseNestedOptions = let evaled = lib.evalModules { - modules = [ { options.foo.bar = lib.mkOption { type = lib.types.bool; }; } ]; + modules = [ + { + options.foo.bar = lib.mkOption { + type = lib.types.bool; + }; + options.foo.baz = lib.mkOption { + type = lib.types.bool; + default = false; + }; + } + ]; }; in { @@ -40,12 +50,23 @@ }; type = "boolean"; }; + baz = { + "$exportedModuleInfo" = { + path = [ + "foo" + "baz" + ]; + }; + type = "boolean"; + default = false; + }; }; required = [ "bar" ]; type = "object"; }; }; type = "object"; + required = [ "foo" ]; }; };