Repository: coder/terraform-provider-coder
Pull request #381: feat: parameter validation refactored to single function
Author: login: Emyrk
Created: 2025-04-14T22:08:33Z | Merged: 2025-05-07T13:01:02Z

# What this PR does

Centralizes all validation logic into a single function on the parameter. This is intended to be imported by other packages (eg: preview) to do clientside validation before running terraform.

# Validation changes

See diff from `main`. Left side is the tests passing on `main`.
https://www.diffchecker.com/GaUlaTze/


- Input values **must** be in the option set. [Before](https://github.com/coder/terraform-provider-coder/blob/a2bb4c074b3257ac7e2fd361b7dea1cadf954206/provider/testdata/parameter_table.md?plain=1#L49) this was allowed, but enforced in `coder/coder`
- Number parameters require values to be numbers. [Before](https://github.com/coder/terraform-provider-coder/blob/a2bb4c074b3257ac7e2fd361b7dea1cadf954206/provider/testdata/parameter_table.md?plain=1#L43) this was allowed, but enforced in `coder/coder`

# Future work 

- Disallow empty values for creating workspaces: https://github.com/coder/terraform-provider-coder/blob/247df4e410c3c9ca159d59a8428975e5e873dc67/provider/parameter.go#L421-L427
- coder/coder to set the correct validation mode
- validate `monotonic` in the provider, passing in the old value via env var

[empty]: https://github.com/coder/terraform-provider-coder/blob/a2bb4c074b3257ac7e2fd361b7dea1cadf954206/provider/testdata/parameter_table.md?plain=1#L4-L14
[option]: https://github.com/coder/terraform-provider-coder/blob/a2bb4c074b3257ac7e2fd361b7dea1cadf954206/provider/testdata/parameter_table.md?plain=1#L20
[default]: https://github.com/coder/terraform-provider-coder/blob/a2bb4c074b3257ac7e2fd361b7dea1cadf954206/provider/testdata/parameter_table.md?plain=1#L46





Diff:
diff --git a/provider/parameter.go b/provider/parameter.go
index 2f7dc662..fd484578 100644
--- a/provider/parameter.go
+++ b/provider/parameter.go
@@ -21,6 +21,10 @@ import (
 	"golang.org/x/xerrors"
 )
 
+var (
+	defaultValuePath = cty.Path{cty.GetAttrStep{Name: "default"}}
+)
+
 type Option struct {
 	Name        string
 	Description string
@@ -46,14 +50,13 @@ const (
 )
 
 type Parameter struct {
-	Value       string
 	Name        string
 	DisplayName string `mapstructure:"display_name"`
 	Description string
 	Type        OptionType
 	FormType    ParameterFormType
 	Mutable     bool
-	Default     string
+	Default     *string
 	Icon        string
 	Option      []Option
 	Validation  []Validation
@@ -82,7 +85,6 @@ func parameterDataSource() *schema.Resource {
 
 			var parameter Parameter
 			err = mapstructure.Decode(struct {
-				Value       interface{}
 				Name        interface{}
 				DisplayName interface{}
 				Description interface{}
@@ -97,17 +99,22 @@ func parameterDataSource() *schema.Resource {
 				Order       interface{}
 				Ephemeral   interface{}
 			}{
-				Value:       rd.Get("value"),
 				Name:        rd.Get("name"),
 				DisplayName: rd.Get("display_name"),
 				Description: rd.Get("description"),
 				Type:        rd.Get("type"),
 				FormType:    rd.Get("form_type"),
 				Mutable:     rd.Get("mutable"),
-				Default:     rd.Get("default"),
-				Icon:        rd.Get("icon"),
-				Option:      rd.Get("option"),
-				Validation:  fixedValidation,
+				Default: func() *string {
+					if rd.GetRawConfig().AsValueMap()["default"].IsNull() {
+						return nil
+					}
+					val, _ := rd.Get("default").(string)
+					return &val
+				}(),
+				Icon:       rd.Get("icon"),
+				Option:     rd.Get("option"),
+				Validation: fixedValidation,
 				Optional: func() bool {
 					// This hack allows for checking if the "default" field is present in the .tf file.
 					// If "default" is missing or is "null", then it means that this field is required,
@@ -122,19 +129,6 @@ func parameterDataSource() *schema.Resource {
 			if err != nil {
 				return diag.Errorf("decode parameter: %s", err)
 			}
-			var value string
-			if parameter.Default != "" {
-				err := valueIsType(parameter.Type, parameter.Default, cty.Path{cty.GetAttrStep{Name: "default"}})
-				if err != nil {
-					return err
-				}
-				value = parameter.Default
-			}
-			envValue, ok := os.LookupEnv(ParameterEnvironmentVariable(parameter.Name))
-			if ok {
-				value = envValue
-			}
-			rd.Set("value", value)
 
 			if !parameter.Mutable && parameter.Ephemeral {
 				return diag.Errorf("parameter can't be immutable and ephemeral")
@@ -144,38 +138,25 @@ func parameterDataSource() *schema.Resource {
 				return diag.Errorf("ephemeral parameter requires the default property")
 			}
 
-			// TODO: Should we move this into the Valid() function on
-			//  Parameter?
-			if len(parameter.Validation) == 1 {
-				validation := &parameter.Validation[0]
-				err = validation.Valid(parameter.Type, value)
-				if err != nil {
-					return diag.FromErr(err)
-				}
-			}
-
-			// Validate options
-			_, parameter.FormType, err = ValidateFormType(parameter.Type, len(parameter.Option), parameter.FormType)
-			if err != nil {
-				return diag.Diagnostics{
-					{
-						Severity:      diag.Error,
-						Summary:       "Invalid form_type for parameter",
-						Detail:        err.Error(),
-						AttributePath: cty.Path{cty.GetAttrStep{Name: "form_type"}},
-					},
-				}
+			var input *string
+			envValue, ok := os.LookupEnv(ParameterEnvironmentVariable(parameter.Name))
+			if ok {
+				input = &envValue
 			}
-			// Set the form_type back in case the value was changed.
-			// Eg via a default. If a user does not specify, a default value
-			// is used and saved.
-			rd.Set("form_type", parameter.FormType)
 
-			diags := parameter.Valid()
+			value, diags := parameter.ValidateInput(input)
 			if diags.HasError() {
 				return diags
 			}
 
+			// Always set back the value, as it can be sourced from the default
+			rd.Set("value", value)
+
+			// Set the form_type, as if it was unset, a default form_type will be updated on
+			// the parameter struct. Always set back the updated form_type to be more
+			// specific than the default empty string.
+			rd.Set("form_type", parameter.FormType)
+
 			return nil
 		},
 		Schema: map[string]*schema.Schema{
@@ -389,37 +370,49 @@ func fixValidationResourceData(rawConfig cty.Value, validation interface{}) (int
 	return vArr, nil
 }
 
-func valueIsType(typ OptionType, value string, attrPath cty.Path) diag.Diagnostics {
+func valueIsType(typ OptionType, value string) error {
 	switch typ {
 	case OptionTypeNumber:
 		_, err := strconv.ParseFloat(value, 64)
 		if err != nil {
-			return diag.Errorf("%q is not a number", value)
+			return fmt.Errorf("%q is not a number", value)
 		}
 	case OptionTypeBoolean:
 		_, err := strconv.ParseBool(value)
 		if err != nil {
-			return diag.Errorf("%q is not a bool", value)
+			return fmt.Errorf("%q is not a bool", value)
 		}
 	case OptionTypeListString:
-		_, diags := valueIsListString(value, attrPath)
-		if diags.HasError() {
-			return diags
+		_, err := valueIsListString(value)
+		if err != nil {
+			return err
 		}
 	case OptionTypeString:
 		// Anything is a string!
 	default:
-		return diag.Errorf("invalid type %q", typ)
+		return fmt.Errorf("invalid type %q", typ)
 	}
 	return nil
 }
 
-func (v *Parameter) Valid() diag.Diagnostics {
+func (v *Parameter) ValidateInput(input *string) (string, diag.Diagnostics) {
+	var err error
+	var optionType OptionType
+
+	valuePath := cty.Path{}
+	value := input
+	if input == nil {
+		value = v.Default
+		if v.Default != nil {
+			valuePath = defaultValuePath
+		}
+	}
+
 	// optionType might differ from parameter.Type. This is ok, and parameter.Type
 	// should be used for the value type, and optionType for options.
-	optionType, _, err := ValidateFormType(v.Type, len(v.Option), v.FormType)
+	optionType, v.FormType, err = ValidateFormType(v.Type, len(v.Option), v.FormType)
 	if err != nil {
-		return diag.Diagnostics{
+		return "", diag.Diagnostics{
 			{
 				Severity:      diag.Error,
 				Summary:       "Invalid form_type for parameter",
@@ -429,53 +422,120 @@ func (v *Parameter) Valid() diag.Diagnostics {
 		}
 	}
 
-	optionNames := map[string]any{}
-	optionValues := map[string]any{}
-	if len(v.Option) > 0 {
-		for _, option := range v.Option {
-			_, exists := optionNames[option.Name]
-			if exists {
-				return diag.Diagnostics{{
-					Severity: diag.Error,
-					Summary:  "Option names must be unique.",
-					Detail:   fmt.Sprintf("multiple options found with the same name %q", option.Name),
-				},
-				}
-			}
-			_, exists = optionValues[option.Value]
-			if exists {
-				return diag.Diagnostics{
-					{
-						Severity: diag.Error,
-						Summary:  "Option values must be unique.",
-						Detail:   fmt.Sprintf("multiple options found with the same value %q", option.Value),
-					},
-				}
-			}
-			diags := valueIsType(optionType, option.Value, cty.Path{})
-			if diags.HasError() {
-				return diags
-			}
-			optionValues[option.Value] = nil
-			optionNames[option.Name] = nil
+	optionValues, diags := v.ValidOptions(optionType)
+	if diags.HasError() {
+		return "", diags
+	}
+
+	// TODO: This is a bit of a hack. The current behavior states if validation
+	//   is given, then apply validation to unset values.
+	//   value == nil should not be accepted in the first place.
+	//   To fix this, value should be coerced to an empty string
+	//   if it is nil. Then let the validation logic always apply.
+	if len(v.Validation) == 0 && value == nil {
+		return "", nil
+	}
+
+	// forcedValue ensures the value is not-nil.
+	var forcedValue string
+	if value != nil {
+		forcedValue = *value
+	}
+
+	d := v.validValue(forcedValue, optionType, optionValues, valuePath)
+	if d.HasError() {
+		return "", d
+	}
+
+	err = valueIsType(v.Type, forcedValue)
+	if err != nil {
+		return "", diag.Diagnostics{
+			{
+				Severity: diag.Error,
+				Summary:  fmt.Sprintf("Parameter value is not of type %q", v.Type),
+				Detail:   err.Error(),
+			},
 		}
 	}
 
-	if v.Default != "" && len(v.Option) > 0 {
+	return forcedValue, nil
+}
+
+func (v *Parameter) ValidOptions(optionType OptionType) (map[string]struct{}, diag.Diagnostics) {
+	optionNames := map[string]struct{}{}
+	optionValues := map[string]struct{}{}
+
+	var diags diag.Diagnostics
+	for _, option := range v.Option {
+		_, exists := optionNames[option.Name]
+		if exists {
+			return nil, diag.Diagnostics{{
+				Severity: diag.Error,
+				Summary:  "Option names must be unique.",
+				Detail:   fmt.Sprintf("multiple options found with the same name %q", option.Name),
+			}}
+		}
+
+		_, exists = optionValues[option.Value]
+		if exists {
+			return nil, diag.Diagnostics{{
+				Severity: diag.Error,
+				Summary:  "Option values must be unique.",
+				Detail:   fmt.Sprintf("multiple options found with the same value %q", option.Value),
+			}}
+		}
+
+		err := valueIsType(optionType, option.Value)
+		if err != nil {
+			diags = append(diags, diag.Diagnostic{
+				Severity: diag.Error,
+				Summary:  fmt.Sprintf("Option %q with value=%q is not of type %q", option.Name, option.Value, optionType),
+				Detail:   err.Error(),
+			})
+			continue
+		}
+		optionValues[option.Value] = struct{}{}
+		optionNames[option.Name] = struct{}{}
+
+		// Option values are assumed to be valid. Do not call validValue on them.
+	}
+
+	if diags != nil && diags.HasError() {
+		return nil, diags
+	}
+	return optionValues, nil
+}
+
+func (v *Parameter) validValue(value string, optionType OptionType, optionValues map[string]struct{}, path cty.Path) diag.Diagnostics {
+	// name is used for constructing more precise error messages.
+	name := "Value"
+	if path.Equals(defaultValuePath) {
+		name = "Default value"
+	}
+
+	// First validate if the value is a valid option
+	if len(optionValues) > 0 {
 		if v.Type == OptionTypeListString && optionType == OptionTypeString {
 			// If the type is list(string) and optionType is string, we have
-			// to ensure all elements of the default exist as options.
-			defaultValues, diags := valueIsListString(v.Default, cty.Path{cty.GetAttrStep{Name: "default"}})
-			if diags.HasError() {
-				return diags
+			// to ensure all elements of the value exist as options.
+			listValues, err := valueIsListString(value)
+			if err != nil {
+				return diag.Diagnostics{
+					{
+						Severity:      diag.Error,
+						Summary:       "When using list(string) type, value must be a json encoded list of strings",
+						Detail:        err.Error(),
+						AttributePath: path,
+					},
+				}
 			}
 
 			// missing is used to construct a more helpful error message
 			var missing []string
-			for _, defaultValue := range defaultValues {
-				_, defaultIsValid := optionValues[defaultValue]
-				if !defaultIsValid {
-					missing = append(missing, defaultValue)
+			for _, listValue := range listValues {
+				_, isValid := optionValues[listValue]
+				if !isValid {
+					missing = append(missing, listValue)
 				}
 			}
 
@@ -483,30 +543,49 @@ func (v *Parameter) Valid() diag.Diagnostics {
 				return diag.Diagnostics{
 					{
 						Severity: diag.Error,
-						Summary:  "Default values must be a valid option",
+						Summary:  fmt.Sprintf("%ss must be a valid option", name),
 						Detail: fmt.Sprintf(
-							"default value %q is not a valid option, values %q are missing from the options",
-							v.Default, strings.Join(missing, ", "),
+							"%s %q is not a valid option, values %q are missing from the options",
+							name, value, strings.Join(missing, ", "),
 						),
-						AttributePath: cty.Path{cty.GetAttrStep{Name: "default"}},
+						AttributePath: path,
 					},
 				}
 			}
 		} else {
-			_, defaultIsValid := optionValues[v.Default]
-			if !defaultIsValid {
+			_, isValid := optionValues[value]
+			if !isValid {
+				extra := ""
+				if value == "" {
+					extra = ". The value is empty, did you forget to set it with a default or from user input?"
+				}
 				return diag.Diagnostics{
 					{
 						Severity:      diag.Error,
-						Summary:       "Default value must be a valid option",
-						Detail:        fmt.Sprintf("the value %q must be defined as one of options", v.Default),
-						AttributePath: cty.Path{cty.GetAttrStep{Name: "default"}},
+						Summary:       fmt.Sprintf("%s must be a valid option%s", name, extra),
+						Detail:        fmt.Sprintf("the value %q must be defined as one of options", value),
+						AttributePath: path,
 					},
 				}
 			}
 		}
 	}
 
+	if len(v.Validation) == 1 {
+		validCheck := &v.Validation[0]
+		err := validCheck.Valid(v.Type, value)
+		if err != nil {
+			return diag.Diagnostics{
+				{
+					Severity:      diag.Error,
+					Summary:       fmt.Sprintf("Invalid parameter %s according to 'validation' block", strings.ToLower(name)),
+					Detail:        err.Error(),
+					AttributePath: path,
+				},
+			}
+		}
+	}
+
 	return nil
 }
 
@@ -570,18 +649,11 @@ func (v *Validation) Valid(typ OptionType, value string) error {
 	return nil
 }
 
-func valueIsListString(value string, path cty.Path) ([]string, diag.Diagnostics) {
+func valueIsListString(value string) ([]string, error) {
 	var items []string
 	err := json.Unmarshal([]byte(value), &items)
 	if err != nil {
-		return nil, diag.Diagnostics{
-			{
-				Severity:      diag.Error,
-				Summary:       "When using list(string) type, value must be a json encoded list of strings",
-				Detail:        fmt.Sprintf("value %q is not a valid list of strings", value),
-				AttributePath: path,
-			},
-		}
+		return nil, fmt.Errorf("value %q is not a valid list of strings", value)
 	}
 	return items, nil
 }
diff --git a/provider/parameter_test.go b/provider/parameter_test.go
index 32877c2b..21842b6a 100644
--- a/provider/parameter_test.go
+++ b/provider/parameter_test.go
@@ -10,6 +10,7 @@ import (
 
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 
 	"github.com/coder/terraform-provider-coder/v2/provider"
@@ -84,6 +85,7 @@ func TestParameter(t *testing.T) {
 			data "coder_parameter" "region" {
 				name = "Region"
 				type = "number"
+				default = 1
 				option {
 					name = "1"
 					value = "1"
@@ -101,6 +103,7 @@ func TestParameter(t *testing.T) {
 			data "coder_parameter" "region" {
 				name = "Region"
 				type = "string"
+				default = "1"
 				option {
 					name = "1"
 					value = "1"
@@ -688,6 +691,168 @@ data "coder_parameter" "region" {
 	}
 }
 
+func TestParameterValidation(t *testing.T) {
+	t.Parallel()
+	opts := func(vals ...string) []provider.Option {
+		options := make([]provider.Option, 0, len(vals))
+		for _, val := range vals {
+			options = append(options, provider.Option{
+				Name:  val,
+				Value: val,
+			})
+		}
+		return options
+	}
+
+	for _, tc := range []struct {
+		Name        string
+		Parameter   provider.Parameter
+		Value       string
+		ExpectError *regexp.Regexp
+	}{
+		{
+			Name: "ValidStringParameter",
+			Parameter: provider.Parameter{
+				Type: "string",
+			},
+			Value: "alpha",
+		},
+		// Test invalid states
+		{
+			Name: "InvalidFormType",
+			Parameter: provider.Parameter{
+				Type:     "string",
+				Option:   opts("alpha", "bravo", "charlie"),
+				FormType: provider.ParameterFormTypeSlider,
+			},
+			Value:       "alpha",
+			ExpectError: regexp.MustCompile("Invalid form_type for parameter"),
+		},
+		{
+			Name: "NotInOptions",
+			Parameter: provider.Parameter{
+				Type:   "string",
+				Option: opts("alpha", "bravo", "charlie"),
+			},
+			Value:       "delta", // not in option set
+			ExpectError: regexp.MustCompile("Value must be a valid option"),
+		},
+		{
+			Name: "NumberNotInOptions",
+			Parameter: provider.Parameter{
+				Type:   "number",
+				Option: opts("1", "2", "3"),
+			},
+			Value:       "0", // not in option set
+			ExpectError: regexp.MustCompile("Value must be a valid option"),
+		},
+		{
+			Name: "NonUniqueOptionNames",
+			Parameter: provider.Parameter{
+				Type:   "string",
+				Option: opts("alpha", "alpha"),
+			},
+			Value:       "alpha",
+			ExpectError: regexp.MustCompile("Option names must be unique"),
+		},
+		{
+			Name: "NonUniqueOptionValues",
+			Parameter: provider.Parameter{
+				Type: "string",
+				Option: []provider.Option{
+					{Name: "Alpha", Value: "alpha"},
+					{Name: "AlphaAgain", Value: "alpha"},
+				},
+			},
+			Value:       "alpha",
+			ExpectError: regexp.MustCompile("Option values must be unique"),
+		},
+		{
+			Name: "IncorrectValueTypeOption",
+			Parameter: provider.Parameter{
+				Type:   "number",
+				Option: opts("not-a-number"),
+			},
+			Value:       "5",
+			ExpectError: regexp.MustCompile("is not a number"),
+		},
+		{
+			Name: "IncorrectValueType",
+			Parameter: provider.Parameter{
+				Type: "number",
+			},
+			Value:       "not-a-number",
+			ExpectError: regexp.MustCompile("Parameter value is not of type \"number\""),
+		},
+		{
+			Name: "NotListStringDefault",
+			Parameter: provider.Parameter{
+				Type:    "list(string)",
+				Default: ptr("not-a-list"),
+			},
+			ExpectError: regexp.MustCompile("not a valid list of strings"),
+		},
+		{
+			Name: "NotListStringDefault",
+			Parameter: provider.Parameter{
+				Type: "list(string)",
+			},
+			Value:       "not-a-list",
+			ExpectError: regexp.MustCompile("not a valid list of strings"),
+		},
+		{
+			Name: "DefaultListStringNotInOptions",
+			Parameter: provider.Parameter{
+				Type:     "list(string)",
+				Default:  ptr(`["red", "yellow", "black"]`),
+				Option:   opts("red", "blue", "green"),
+				FormType: provider.ParameterFormTypeMultiSelect,
+			},
+			Value:       `["red", "yellow", "black"]`,
+			ExpectError: regexp.MustCompile("is not a valid option, values \"yellow, black\" are missing from the options"),
+		},
+		{
+			Name: "ListStringNotInOptions",
+			Parameter: provider.Parameter{
+				Type:     "list(string)",
+				Default:  ptr(`["red"]`),
+				Option:   opts("red", "blue", "green"),
+				FormType: provider.ParameterFormTypeMultiSelect,
+			},
+			Value:       `["red", "yellow", "black"]`,
+			ExpectError: regexp.MustCompile("is not a valid option, values \"yellow, black\" are missing from the options"),
+		},
+		{
+			Name: "InvalidMiniumum",
+			Parameter: provider.Parameter{
+				Type:    "number",
+				Default: ptr("5"),
+				Validation: []provider.Validation{{
+					Min:   10,
+					Error: "must be greater than 10",
+				}},
+			},
+			ExpectError: regexp.MustCompile("must be greater than 10"),
+		},
+	} {
+		tc := tc
+		t.Run(tc.Name, func(t *testing.T) {
+			t.Parallel()
+			value := &tc.Value
+			_, diags := tc.Parameter.ValidateInput(value)
+			if tc.ExpectError != nil {
+				require.True(t, diags.HasError())
+				errMsg := fmt.Sprintf("%+v", diags[0]) // close enough
+				require.Truef(t, tc.ExpectError.MatchString(errMsg), "got: %s", errMsg)
+			} else {
+				if !assert.False(t, diags.HasError()) {
+					t.Logf("got: %+v", diags[0])
+				}
+			}
+		})
+	}
+}
+
 // TestParameterValidationEnforcement tests various parameter states and the
 // validation enforcement that should be applied to them. The table is described
 // by a markdown table. This is done so that the test cases can be more easily
@@ -703,10 +868,6 @@ func TestParameterValidationEnforcement(t *testing.T) {
 	// - Validation logic does not apply to the default if a value is given
 	//	- [NumIns/DefInv] So the default can be invalid if an input value is valid.
 	//	  The value is therefore not really optional, but it is marked as such.
-	// - [NumInsNotOptsVal | NumsInsNotOpts] values do not need to be in the option set?
-	// - [NumInsNotNum] number params do not require the value to be a number
-	// - [LStrInsNotList] list(string) do not require the value to be a list(string)
-	//	- Same with [MulInsNotListOpts]
 	table, err := os.ReadFile("testdata/parameter_table.md")
 	require.NoError(t, err)
 
@@ -719,7 +880,7 @@ func TestParameterValidationEnforcement(t *testing.T) {
 		Validation  *provider.Validation
 		OutputValue string
 		Optional    bool
-		Error       *regexp.Regexp
+		CreateError *regexp.Regexp
 	}
 
 	rows := make([]row, 0)
@@ -750,6 +911,7 @@ func TestParameterValidationEnforcement(t *testing.T) {
 				t.Fatalf("failed to parse error column %q: %v", columns[9], err)
 			}
 		}
+
 		var options []string
 		if columns[4] != "" {
 			options = strings.Split(columns[4], ",")
@@ -796,7 +958,7 @@ func TestParameterValidationEnforcement(t *testing.T) {
 			Validation:  validation,
 			OutputValue: columns[7],
 			Optional:    optional,
-			Error:       rerr,
+			CreateError: rerr,
 		})
 	}
 
@@ -815,10 +977,8 @@ func TestParameterValidationEnforcement(t *testing.T) {
 					t.Setenv(provider.ParameterEnvironmentVariable("parameter"), row.InputValue)
 				}
 
-				if row.Error != nil {
-					if row.OutputValue != "" {
-						t.Errorf("output value %q should not be set if error is set", row.OutputValue)
-					}
+				if row.CreateError != nil && row.OutputValue != "" {
+					t.Errorf("output value %q should not be set if both errors are set", row.OutputValue)
 				}
 
 				var cfg strings.Builder
@@ -860,13 +1020,12 @@ func TestParameterValidationEnforcement(t *testing.T) {
 				}
 
 				cfg.WriteString("}\n")
-
 				resource.Test(t, resource.TestCase{
 					ProviderFactories: coderFactory(),
 					IsUnitTest:        true,
 					Steps: []resource.TestStep{{
 						Config:      cfg.String(),
-						ExpectError: row.Error,
+						ExpectError: row.CreateError,
 						Check: func(state *terraform.State) error {
 							require.Len(t, state.Modules, 1)
 							require.Len(t, state.Modules[0].Resources, 1)
@@ -1096,3 +1255,7 @@ func TestParameterWithManyOptions(t *testing.T) {
 		}},
 	})
 }
+
+func ptr[T any](v T) *T {
+	return &v
+}
diff --git a/provider/testdata/parameter_table.md b/provider/testdata/parameter_table.md
index f7645efa..3df16f06 100644
--- a/provider/testdata/parameter_table.md
+++ b/provider/testdata/parameter_table.md
@@ -1,79 +1,80 @@
-| Name                 | Type          | Input     | Default | Options           | Validation | -> | Output | Optional | Error        |
-|----------------------|---------------|-----------|---------|-------------------|------------|----|--------|----------|--------------|
-|                      | Empty Vals    |           |         |                   |            |    |        |          |              |
-| Empty                | string,number |           |         |                   |            |    | ""     | false    |              |
-| EmptyDupeOps         | string,number |           |         | 1,1,1             |            |    |        |          | unique       |
-| EmptyList            | list(string)  |           |         |                   |            |    | ""     | false    |              |
-| EmptyListDupeOpts    | list(string)  |           |         | ["a"],["a"]       |            |    |        |          | unique       |
-| EmptyMulti           | tag-select    |           |         |                   |            |    | ""     | false    |              |
-| EmptyOpts            | string,number |           |         | 1,2,3             |            |    | ""     | false    |              |
-| EmptyRegex           | string        |           |         |                   | world      |    |        |          | regex error  |
-| EmptyMin             | number        |           |         |                   | 1-10       |    |        |          | 1 <  < 10    |
-| EmptyMinOpt          | number        |           |         | 1,2,3             | 2-5        |    |        |          | 2 <  < 5     |
-| EmptyRegexOpt        | string        |           |         | "hello","goodbye" | goodbye    |    |        |          | regex error  |
-| EmptyRegexOk         | string        |           |         |                   | .*         |    | ""     | false    |              |
-|                      |               |           |         |                   |            |    |        |          |              |
-|                      | Default Set   | No inputs |         |                   |            |    |        |          |              |
-| NumDef               | number        |           | 5       |                   |            |    | 5      | true     |              |
-| NumDefVal            | number        |           | 5       |                   | 3-7        |    | 5      | true     |              |
-| NumDefInv            | number        |           | 5       |                   | 10-        |    |        |          | 10 < 5 < 0   |
-| NumDefOpts           | number        |           | 5       | 1,3,5,7           | 2-6        |    | 5      | true     |              |
-| NumDefNotOpts        | number        |           | 5       | 1,3,7,9           | 2-6        |    |        |          | valid option |
-| NumDefInvOpt         | number        |           | 5       | 1,3,5,7           | 6-10       |    |        |          | 6 < 5 < 10   |
-| NumDefNotNum         | number        |           | a       |                   |            |    |        |          | a number     |
-| NumDefOptsNotNum     | number        |           | 1       | 1,a,2             |            |    |        |          | a number     |
-|                      |               |           |         |                   |            |    |        |          |              |
-| StrDef               | string        |           | hello   |                   |            |    | hello  | true     |              |
-| StrDefInv            | string        |           | hello   |                   | world      |    |        |          | regex error  |
-| StrDefOpts           | string        |           | a       | a,b,c             |            |    | a      | true     |              |
-| StrDefNotOpts        | string        |           | a       | b,c,d             |            |    |        |          | valid option |
-| StrDefValOpts        | string        |           | a       | a,b,c,d,e,f       | [a-c]      |    | a      | true     |              |
-| StrDefInvOpt         | string        |           | d       | a,b,c,d,e,f       | [a-c]      |    |        |          | regex error  |
-|                      |               |           |         |                   |            |    |        |          |              |
-| LStrDef              | list(string)  |           | ["a"]   |                   |            |    | ["a"]  | true     |              |
-| LStrDefOpts          | list(string)  |           | ["a"]   | ["a"], ["b"]      |            |    | ["a"]  | true     |              |
-| LStrDefNotOpts       | list(string)  |           | ["a"]   | ["b"], ["c"]      |            |    |        |          | valid option |
-|                      |               |           |         |                   |            |    |        |          |              |
-| MulDef               | tag-select    |           | ["a"]   |                   |            |    | ["a"]  | true     |              |
-| MulDefOpts           | multi-select  |           | ["a"]   | a,b               |            |    | ["a"]  | true     |              |
-| MulDefNotOpts        | multi-select  |           | ["a"]   | b,c               |            |    |        |          | valid option |
-|                      |               |           |         |                   |            |    |        |          |              |
-|                      | Input Vals    |           |         |                   |            |    |        |          |              |
-| NumIns               | number        | 3         |         |                   |            |    | 3      | false    |              |
-| NumInsNotNum         | number        | a         |         |                   |            |    | a      | false    |              |
-| NumInsNotNumInv      | number        | a         |         |                   | 1-3        |    |        |          | 1 < a < 3    |
-| NumInsDef            | number        | 3         | 5       |                   |            |    | 3      | true     |              |
-| NumIns/DefInv        | number        | 3         | 5       |                   | 1-3        |    | 3      | true     |              |
-| NumIns=DefInv        | number        | 5         | 5       |                   | 1-3        |    |        |          | 1 < 5 < 3    |
-| NumInsOpts           | number        | 3         | 5       | 1,2,3,4,5         | 1-3        |    | 3      | true     |              |
-| NumInsNotOptsVal     | number        | 3         | 5       | 1,2,4,5           | 1-3        |    | 3      | true     |              |
-| NumInsNotOptsInv     | number        | 3         | 5       | 1,2,4,5           | 1-2        |    |        | true     | 1 < 3 < 2    |
-| NumInsNotOpts        | number        | 3         | 5       | 1,2,4,5           |            |    | 3      | true     |              |
-| NumInsNotOpts/NoDef  | number        | 3         |         | 1,2,4,5           |            |    | 3      | false    |              |
-|                      |               |           |         |                   |            |    |        |          |              |
-| StrIns               | string        | c         |         |                   |            |    | c      | false    |              |
-| StrInsDupeOpts       | string        | c         |         | a,b,c,c           |            |    |        |          | unique       |
-| StrInsDef            | string        | c         | e       |                   |            |    | c      | true     |              |
-| StrIns/DefInv        | string        | c         | e       |                   | [a-c]      |    | c      | true     |              |
-| StrIns=DefInv        | string        | e         | e       |                   | [a-c]      |    |        |          | regex error  |
-| StrInsOpts           | string        | c         | e       | a,b,c,d,e         | [a-c]      |    | c      | true     |              |
-| StrInsNotOptsVal     | string        | c         | e       | a,b,d,e           | [a-c]      |    | c      | true     |              |
-| StrInsNotOptsInv     | string        | c         | e       | a,b,d,e           | [a-b]      |    |        |          | regex error  |
-| StrInsNotOpts        | string        | c         | e       | a,b,d,e           |            |    | c      | true     |              |
-| StrInsNotOpts/NoDef  | string        | c         |         | a,b,d,e           |            |    | c      | false    |              |
-| StrInsBadVal         | string        | c         |         | a,b,c,d,e         | 1-10       |    |        |          | min cannot   |
-|                      |               |           |         |                   |            |    |        |          |              |
-|                      | list(string)  |           |         |                   |            |    |        |          |              |
-| LStrIns              | list(string)  | ["c"]     |         |                   |            |    | ["c"]  | false    |              |
-| LStrInsNotList       | list(string)  | c         |         |                   |            |    | c      | false    |              |
-| LStrInsDef           | list(string)  | ["c"]     | ["e"]   |                   |            |    | ["c"]  | true     |              |
-| LStrIns/DefInv       | list(string)  | ["c"]     | ["e"]   |                   | [a-c]      |    |        |          | regex cannot |
-| LStrInsOpts          | list(string)  | ["c"]     | ["e"]   | ["c"],["d"],["e"] |            |    | ["c"]  | true     |              |
-| LStrInsNotOpts       | list(string)  | ["c"]     | ["e"]   | ["d"],["e"]       |            |    | ["c"]  | true     |              |
-| LStrInsNotOpts/NoDef | list(string)  | ["c"]     |         | ["d"],["e"]       |            |    | ["c"]  | false    |              |
-|                      |               |           |         |                   |            |    |        |          |              |
-| MulInsOpts           | multi-select  | ["c"]     | ["e"]   | c,d,e             |            |    | ["c"]  | true     |              |
-| MulInsNotListOpts    | multi-select  | c         | ["e"]   | c,d,e             |            |    | c      | true     |              |
-| MulInsNotOpts        | multi-select  | ["c"]     | ["e"]   | d,e               |            |    | ["c"]  | true     |              |
-| MulInsNotOpts/NoDef  | multi-select  | ["c"]     |         | d,e               |            |    | ["c"]  | false    |              |
-| MulInsInvOpts        | multi-select  | ["c"]     | ["e"]   | c,d,e             | [a-c]      |    |        |          | regex cannot |
\ No newline at end of file
+| Name                 | Type          | Input     | Default | Options           | Validation | -> | Output | Optional | ErrorCreate     |
+|----------------------|---------------|-----------|---------|-------------------|------------|----|--------|----------|-----------------|
+|                      | Empty Vals    |           |         |                   |            |    |        |          |                 |
+| Empty                | string,number |           |         |                   |            |    | ""     | false    |                 |
+| EmptyDupeOps         | string,number |           |         | 1,1,1             |            |    |        |          | unique          |
+| EmptyList            | list(string)  |           |         |                   |            |    | ""     | false    |                 |
+| EmptyListDupeOpts    | list(string)  |           |         | ["a"],["a"]       |            |    |        |          | unique          |
+| EmptyMulti           | tag-select    |           |         |                   |            |    | ""     | false    |                 |
+| EmptyOpts            | string,number |           |         | 1,2,3             |            |    | ""     | false    |                 |
+| EmptyRegex           | string        |           |         |                   | world      |    |        |          | regex error     |
+| EmptyMin             | number        |           |         |                   | 1-10       |    |        |          | 1 <  < 10       |
+| EmptyMinOpt          | number        |           |         | 1,2,3             | 2-5        |    |        |          | valid option    |
+| EmptyRegexOpt        | string        |           |         | "hello","goodbye" | goodbye    |    |        |          | valid option    |
+| EmptyRegexOk         | string        |           |         |                   | .*         |    | ""     | false    |                 |
+|                      |               |           |         |                   |            |    |        |          |                 |
+|                      | Default Set   | No inputs |         |                   |            |    |        |          |                 |
+| NumDef               | number        |           | 5       |                   |            |    | 5      | true     |                 |
+| NumDefVal            | number        |           | 5       |                   | 3-7        |    | 5      | true     |                 |
+| NumDefInv            | number        |           | 5       |                   | 10-        |    |        |          | 10 < 5 < 0      |
+| NumDefOpts           | number        |           | 5       | 1,3,5,7           | 2-6        |    | 5      | true     |                 |
+| NumDefNotOpts        | number        |           | 5       | 1,3,7,9           | 2-6        |    |        |          | valid option    |
+| NumDefInvOpt         | number        |           | 5       | 1,3,5,7           | 6-10       |    |        |          | 6 < 5 < 10      |
+| NumDefNotNum         | number        |           | a       |                   |            |    |        |          | type "number"   |
+| NumDefOptsNotNum     | number        |           | 1       | 1,a,2             |            |    |        |          | type "number"   |
+|                      |               |           |         |                   |            |    |        |          |                 |
+| StrDef               | string        |           | hello   |                   |            |    | hello  | true     |                 |
+| StrDefInv            | string        |           | hello   |                   | world      |    |        |          | regex error     |
+| StrDefOpts           | string        |           | a       | a,b,c             |            |    | a      | true     |                 |
+| StrDefNotOpts        | string        |           | a       | b,c,d             |            |    |        |          | valid option    |
+| StrDefValOpts        | string        |           | a       | a,b,c,d,e,f       | [a-c]      |    | a      | true     |                 |
+| StrDefInvOpt         | string        |           | d       | a,b,c,d,e,f       | [a-c]      |    |        |          | regex error     |
+|                      |               |           |         |                   |            |    |        |          |                 |
+| LStrDef              | list(string)  |           | ["a"]   |                   |            |    | ["a"]  | true     |                 |
+| LStrDefOpts          | list(string)  |           | ["a"]   | ["a"], ["b"]      |            |    | ["a"]  | true     |                 |
+| LStrDefNotOpts       | list(string)  |           | ["a"]   | ["b"], ["c"]      |            |    |        |          | valid option    |
+|                      |               |           |         |                   |            |    |        |          |                 |
+| MulDef               | tag-select    |           | ["a"]   |                   |            |    | ["a"]  | true     |                 |
+| MulDefOpts           | multi-select  |           | ["a"]   | a,b               |            |    | ["a"]  | true     |                 |
+| MulDefNotOpts        | multi-select  |           | ["a"]   | b,c               |            |    |        |          | valid option    |
+|                      |               |           |         |                   |            |    |        |          |                 |
+|                      | Input Vals    |           |         |                   |            |    |        |          |                 |
+| NumIns               | number        | 3         |         |                   |            |    | 3      | false    |                 |
+| NumInsOptsNaN        | number        | 3         | 5       | a,1,2,3,4,5       | 1-3        |    |        |          | type "number"   |
+| NumInsNotNum         | number        | a         |         |                   |            |    |        |          | type "number"   |
+| NumInsNotNumInv      | number        | a         |         |                   | 1-3        |    |        |          | 1 < a < 3       |
+| NumInsDef            | number        | 3         | 5       |                   |            |    | 3      | true     |                 |
+| NumIns/DefInv        | number        | 3         | 5       |                   | 1-3        |    | 3      | true     |                 |
+| NumIns=DefInv        | number        | 5         | 5       |                   | 1-3        |    |        |          | 1 < 5 < 3       |
+| NumInsOpts           | number        | 3         | 5       | 1,2,3,4,5         | 1-3        |    | 3      | true     |                 |
+| NumInsNotOptsVal     | number        | 3         | 5       | 1,2,4,5           | 1-3        |    |        |          | valid option    |
+| NumInsNotOptsInv     | number        | 3         | 5       | 1,2,4,5           | 1-2        |    |        | true     | valid option    |
+| NumInsNotOpts        | number        | 3         | 5       | 1,2,4,5           |            |    |        |          | valid option    |
+| NumInsNotOpts/NoDef  | number        | 3         |         | 1,2,4,5           |            |    |        |          | valid option    |
+|                      |               |           |         |                   |            |    |        |          |                 |
+| StrIns               | string        | c         |         |                   |            |    | c      | false    |                 |
+| StrInsDupeOpts       | string        | c         |         | a,b,c,c           |            |    |        |          | unique          |
+| StrInsDef            | string        | c         | e       |                   |            |    | c      | true     |                 |
+| StrIns/DefInv        | string        | c         | e       |                   | [a-c]      |    | c      | true     |                 |
+| StrIns=DefInv        | string        | e         | e       |                   | [a-c]      |    |        |          | regex error     |
+| StrInsOpts           | string        | c         | e       | a,b,c,d,e         | [a-c]      |    | c      | true     |                 |
+| StrInsNotOptsVal     | string        | c         | e       | a,b,d,e           | [a-c]      |    |        |          | valid option    |
+| StrInsNotOptsInv     | string        | c         | e       | a,b,d,e           | [a-b]      |    |        |          | valid option    |
+| StrInsNotOpts        | string        | c         | e       | a,b,d,e           |            |    |        |          | valid option    |
+| StrInsNotOpts/NoDef  | string        | c         |         | a,b,d,e           |            |    |        |          | valid option    |
+| StrInsBadVal         | string        | c         |         | a,b,c,d,e         | 1-10       |    |        |          | min cannot      |
+|                      |               |           |         |                   |            |    |        |          |                 |
+|                      | list(string)  |           |         |                   |            |    |        |          |                 |
+| LStrIns              | list(string)  | ["c"]     |         |                   |            |    | ["c"]  | false    |                 |
+| LStrInsNotList       | list(string)  | c         |         |                   |            |    |        |          | list of strings |
+| LStrInsDef           | list(string)  | ["c"]     | ["e"]   |                   |            |    | ["c"]  | true     |                 |
+| LStrIns/DefInv       | list(string)  | ["c"]     | ["e"]   |                   | [a-c]      |    |        |          | regex cannot    |
+| LStrInsOpts          | list(string)  | ["c"]     | ["e"]   | ["c"],["d"],["e"] |            |    | ["c"]  | true     |                 |
+| LStrInsNotOpts       | list(string)  | ["c"]     | ["e"]   | ["d"],["e"]       |            |    |        |          | valid option    |
+| LStrInsNotOpts/NoDef | list(string)  | ["c"]     |         | ["d"],["e"]       |            |    |        |          | valid option    |
+|                      |               |           |         |                   |            |    |        |          |                 |
+| MulInsOpts           | multi-select  | ["c"]     | ["e"]   | c,d,e             |            |    | ["c"]  | true     |                 |
+| MulInsNotListOpts    | multi-select  | c         | ["e"]   | c,d,e             |            |    |        |          | json encoded    |
+| MulInsNotOpts        | multi-select  | ["c"]     | ["e"]   | d,e               |            |    |        |          | valid option    |
+| MulInsNotOpts/NoDef  | multi-select  | ["c"]     |         | d,e               |            |    |        |          | valid option    |
+| MulInsInvOpts        | multi-select  | ["c"]     | ["e"]   | c,d,e             | [a-c]      |    |        |          | regex cannot    |
\ No newline at end of file


Reviews:
user: login: spikecurtis
body: It seems very odd to me that in this PR, template-import mode, which is supposedly being created for back-compatibility is _more_ strict than what you're calling default mode.

Shouldn't it be the other way around? That template-import mode skips some checks that would break backward compatibility?
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T06:22:59Z
user: login: Emyrk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-02T13:33:41Z
user: login: Emyrk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-02T13:46:00Z
user: login: Emyrk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-02T14:20:44Z
user: login: Emyrk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-02T14:22:14Z
user: login: Emyrk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-02T14:25:45Z
user: login: spikecurtis
body: some comments and suggestions inline, but I don't need to review again.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T08:00:27Z
user: login: Emyrk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T12:46:52Z
user: login: Emyrk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T12:50:30Z
user: login: Emyrk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T12:51:36Z

Review comments:
diff_hunk: @@ -429,84 +450,201 @@ func (v *Parameter) Valid() diag.Diagnostics {
 		}
 	}
 
-	optionNames := map[string]any{}
-	optionValues := map[string]any{}
-	if len(v.Option) > 0 {
-		for _, option := range v.Option {
-			_, exists := optionNames[option.Name]
-			if exists {
-				return diag.Diagnostics{{
+	optionValues, diags := v.ValidOptions(optionType, mode)
+	if diags.HasError() {
+		return "", diags
+	}
+
+	if mode == ValidationModeTemplateImport && v.Default != nil {
+		// Template import should validate the default value.
+		err := valueIsType(v.Type, *v.Default)
+		if err != nil {
+			return "", diag.Diagnostics{
+				{
+					Severity:      diag.Error,
+					Summary:       fmt.Sprintf("Default value is not of type %q", v.Type),
+					Detail:        err.Error(),
+					AttributePath: defaultValuePath,
+				},
+			}
+		}
+
+		d := v.validValue(*v.Default, optionType, optionValues, defaultValuePath)
+		if d.HasError() {
+			return "", d
+		}
+	}
+
+	// TODO: This is a bit of a hack. The current behavior states if validation
+	//   is given, then apply validation to unset values.
+	//   This should be removed, and all values should be validated. Meaning
+	//   value == nil should not be accepted in the first place.
+	if len(v.Validation) > 0 && value == nil {
+		empty := ""
+		value = &empty
+	}
+
+	// Value is only validated if it is set. If it is unset, validation
+	// is skipped.
+	if value != nil {
+		d := v.validValue(*value, optionType, optionValues, cty.Path{})
+		if d.HasError() {
+			return "", d
+		}
+
+		err = valueIsType(v.Type, *value)
+		if err != nil {
+			return "", diag.Diagnostics{
+				{
 					Severity: diag.Error,
-					Summary:  "Option names must be unique.",
-					Detail:   fmt.Sprintf("multiple options found with the same name %q", option.Name),
+					Summary:  fmt.Sprintf("Parameter value is not of type %q", v.Type),
+					Detail:   err.Error(),
 				},
-				}
 			}
-			_, exists = optionValues[option.Value]
-			if exists {
-				return diag.Diagnostics{
-					{
-						Severity: diag.Error,
-						Summary:  "Option values must be unique.",
-						Detail:   fmt.Sprintf("multiple options found with the same value %q", option.Value),
-					},
+		}
+	}
+
+	if value == nil {
+		// The previous behavior is to always write an empty string
+		return "", nil
+	}
+
+	return *value, nil
+}
+
+func (v *Parameter) ValidOptions(optionType OptionType, mode ValidationMode) (map[string]struct{}, diag.Diagnostics) {
+	optionNames := map[string]struct{}{}
+	optionValues := map[string]struct{}{}
+
+	var diags diag.Diagnostics
+	for _, option := range v.Option {
+		_, exists := optionNames[option.Name]
+		if exists {
+			return nil, diag.Diagnostics{{
+				Severity: diag.Error,
+				Summary:  "Option names must be unique.",
+				Detail:   fmt.Sprintf("multiple options found with the same name %q", option.Name),
+			}}
+		}
+
+		_, exists = optionValues[option.Value]
+		if exists {
+			return nil, diag.Diagnostics{{
+				Severity: diag.Error,
+				Summary:  "Option values must be unique.",
+				Detail:   fmt.Sprintf("multiple options found with the same value %q", option.Value),
+			}}
+		}
+
+		err := valueIsType(optionType, option.Value)
+		if err != nil {
+			diags = append(diags, diag.Diagnostic{
+				Severity: diag.Error,
+				Summary:  fmt.Sprintf("Option %q with value=%q is not of type %q", option.Name, option.Value, optionType),
+				Detail:   err.Error(),
+			})
+			continue
+		}
+		optionValues[option.Value] = struct{}{}
+		optionNames[option.Name] = struct{}{}
+
+		if mode == ValidationModeTemplateImport {
+			opDiags := v.validValue(option.Value, optionType, nil, cty.Path{})
+			if opDiags.HasError() {
+				for i := range opDiags {
+					opDiags[i].Summary = fmt.Sprintf("Option %q: %s", option.Name, opDiags[i].Summary)
 				}
+				diags = append(diags, opDiags...)
 			}
-			diags := valueIsType(optionType, option.Value, cty.Path{})
-			if diags.HasError() {
-				return diags
-			}
-			optionValues[option.Value] = nil
-			optionNames[option.Name] = nil
 		}
 	}
 
-	if v.Default != "" && len(v.Option) > 0 {
+	if diags.HasError() {
+		return nil, diags
+	}
+	return optionValues, nil
+}
+
+func (v *Parameter) validValue(value string, optionType OptionType, optionValues map[string]struct{}, path cty.Path) diag.Diagnostics {
+	// name is used for constructing more precise error messages.
+	name := "Value"
+	if path.Equals(defaultValuePath) {
+		name = "Default value"
+	}
+
+	// First validate if the value is a valid option
+	if len(optionValues) > 0 {
 		if v.Type == OptionTypeListString && optionType == OptionTypeString {
 			// If the type is list(string) and optionType is string, we have
 			// to ensure all elements of the default exist as options.
-			defaultValues, diags := valueIsListString(v.Default, cty.Path{cty.GetAttrStep{Name: "default"}})
-			if diags.HasError() {
-				return diags
+			listValues, err := valueIsListString(value)
+			if err != nil {
+				return diag.Diagnostics{
+					{
+						Severity:      diag.Error,
+						Summary:       "When using list(string) type, value must be a json encoded list of strings",
+						Detail:        err.Error(),
+						AttributePath: defaultValuePath,
+					},
+				}
 			}
 
 			// missing is used to construct a more helpful error message
 			var missing []string
-			for _, defaultValue := range defaultValues {
-				_, defaultIsValid := optionValues[defaultValue]
-				if !defaultIsValid {
-					missing = append(missing, defaultValue)
+			for _, listValue := range listValues {
+				_, isValid := optionValues[listValue]
+				if !isValid {
+					missing = append(missing, listValue)
 				}
 			}
 
 			if len(missing) > 0 {
 				return diag.Diagnostics{
 					{
 						Severity: diag.Error,
-						Summary:  "Default values must be a valid option",
+						Summary:  fmt.Sprintf("%ss must be a valid option", name),
 						Detail: fmt.Sprintf(
-							"default value %q is not a valid option, values %q are missing from the options",
-							v.Default, strings.Join(missing, ", "),
+							"%s %q is not a valid option, values %q are missing from the options",
+							name, value, strings.Join(missing, ", "),
 						),
-						AttributePath: cty.Path{cty.GetAttrStep{Name: "default"}},
+						AttributePath: defaultValuePath,
 					},
 				}
 			}
 		} else {
-			_, defaultIsValid := optionValues[v.Default]
-			if !defaultIsValid {
+			_, isValid := optionValues[value]
+			if !isValid {
+				extra := ""
+				if value == "" {
+					extra = ". The value is empty, did you forget to set it with a default or from user input?"
+				}
 				return diag.Diagnostics{
 					{
 						Severity:      diag.Error,
-						Summary:       "Default value must be a valid option",
-						Detail:        fmt.Sprintf("the value %q must be defined as one of options", v.Default),
-						AttributePath: cty.Path{cty.GetAttrStep{Name: "default"}},
+						Summary:       fmt.Sprintf("%s must be a valid option%s", name, extra),
+						Detail:        fmt.Sprintf("the value %q must be defined as one of options", value),
+						AttributePath: path,
 					},
 				}
 			}
 		}
 	}
 
+	if len(v.Validation) == 1 {
path: provider/parameter.go
user: login: spikecurtis
body: Is there somewhere we enforce that there can only be a max of one Validation?
created_at: 2025-05-02T06:05:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -429,84 +450,201 @@ func (v *Parameter) Valid() diag.Diagnostics {
 		}
 	}
 
-	optionNames := map[string]any{}
-	optionValues := map[string]any{}
-	if len(v.Option) > 0 {
-		for _, option := range v.Option {
-			_, exists := optionNames[option.Name]
-			if exists {
-				return diag.Diagnostics{{
+	optionValues, diags := v.ValidOptions(optionType, mode)
+	if diags.HasError() {
+		return "", diags
+	}
+
+	if mode == ValidationModeTemplateImport && v.Default != nil {
+		// Template import should validate the default value.
+		err := valueIsType(v.Type, *v.Default)
+		if err != nil {
+			return "", diag.Diagnostics{
+				{
+					Severity:      diag.Error,
+					Summary:       fmt.Sprintf("Default value is not of type %q", v.Type),
+					Detail:        err.Error(),
+					AttributePath: defaultValuePath,
+				},
+			}
+		}
+
+		d := v.validValue(*v.Default, optionType, optionValues, defaultValuePath)
+		if d.HasError() {
+			return "", d
+		}
+	}
+
+	// TODO: This is a bit of a hack. The current behavior states if validation
+	//   is given, then apply validation to unset values.
+	//   This should be removed, and all values should be validated. Meaning
+	//   value == nil should not be accepted in the first place.
+	if len(v.Validation) > 0 && value == nil {
+		empty := ""
+		value = &empty
+	}
path: provider/parameter.go
user: login: spikecurtis
body: I feel like this would be more readable if you inverted the `if` clause and had it `return` in the case that we don't do validation.
created_at: 2025-05-02T06:08:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -429,84 +450,201 @@ func (v *Parameter) Valid() diag.Diagnostics {
 		}
 	}
 
-	optionNames := map[string]any{}
-	optionValues := map[string]any{}
-	if len(v.Option) > 0 {
-		for _, option := range v.Option {
-			_, exists := optionNames[option.Name]
-			if exists {
-				return diag.Diagnostics{{
+	optionValues, diags := v.ValidOptions(optionType, mode)
+	if diags.HasError() {
+		return "", diags
+	}
+
+	if mode == ValidationModeTemplateImport && v.Default != nil {
path: provider/parameter.go
user: login: spikecurtis
body: This is a weird case where template import is _more_ strict than default. What is the justification for that? I thought we were only making template import less strict so that we don't break people.
created_at: 2025-05-02T06:12:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -21,6 +21,25 @@ import (
 	"golang.org/x/xerrors"
 )
 
+type ValidationMode string
+
+const (
+	ValidationModeEnvVar = "CODER_VALIDATION_MODE"
+	// ValidationModeDefault is used for creating a workspace. It validates the final
+	// value used for a parameter. Some allowances for invalid options are tolerated,
+	// as unused options do not affect the final parameter value. The default value
+	// is also ignored, assuming a value is provided.
+	ValidationModeDefault ValidationMode = ""
+	// ValidationModeTemplateImport tolerates empty values, as the value might not be
+	// available at import. It does not tolerate an invalid default or invalid option
path: provider/parameter.go
user: login: spikecurtis
body: IIUC, It doesn't tolerate empty values if there is a validation defined, so this is misleading.
created_at: 2025-05-02T06:13:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -144,38 +153,24 @@ func parameterDataSource() *schema.Resource {
 				return diag.Errorf("ephemeral parameter requires the default property")
 			}
 
-			// TODO: Should we move this into the Valid() function on
-			//  Parameter?
-			if len(parameter.Validation) == 1 {
-				validation := &parameter.Validation[0]
-				err = validation.Valid(parameter.Type, value)
-				if err != nil {
-					return diag.FromErr(err)
-				}
-			}
-
-			// Validate options
-			_, parameter.FormType, err = ValidateFormType(parameter.Type, len(parameter.Option), parameter.FormType)
-			if err != nil {
-				return diag.Diagnostics{
-					{
-						Severity:      diag.Error,
-						Summary:       "Invalid form_type for parameter",
-						Detail:        err.Error(),
-						AttributePath: cty.Path{cty.GetAttrStep{Name: "form_type"}},
-					},
-				}
+			var input *string
+			envValue, ok := os.LookupEnv(ParameterEnvironmentVariable(parameter.Name))
+			if ok {
+				input = &envValue
 			}
-			// Set the form_type back in case the value was changed.
-			// Eg via a default. If a user does not specify, a default value
-			// is used and saved.
-			rd.Set("form_type", parameter.FormType)
 
-			diags := parameter.Valid()
+			mode := os.Getenv(ValidationModeEnvVar)
+			value, diags := parameter.Valid(input, ValidationMode(mode))
path: provider/parameter.go
user: login: spikecurtis
body: Confusing name. Conceptually there is a Parameter resource definition, which might be invalid, and a parameter value, which might also be invalid. This is the latter, but it's defined on the struct that represents the former, so it's confusing. Maybe `parameter.ValidateInput`?
created_at: 2025-05-02T06:15:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -389,37 +384,63 @@ func fixValidationResourceData(rawConfig cty.Value, validation interface{}) (int
 	return vArr, nil
 }
 
-func valueIsType(typ OptionType, value string, attrPath cty.Path) diag.Diagnostics {
+func valueIsType(typ OptionType, value string) error {
 	switch typ {
 	case OptionTypeNumber:
 		_, err := strconv.ParseFloat(value, 64)
 		if err != nil {
-			return diag.Errorf("%q is not a number", value)
+			return fmt.Errorf("%q is not a number", value)
 		}
 	case OptionTypeBoolean:
 		_, err := strconv.ParseBool(value)
 		if err != nil {
-			return diag.Errorf("%q is not a bool", value)
+			return fmt.Errorf("%q is not a bool", value)
 		}
 	case OptionTypeListString:
-		_, diags := valueIsListString(value, attrPath)
-		if diags.HasError() {
-			return diags
+		_, err := valueIsListString(value)
+		if err != nil {
+			return err
 		}
 	case OptionTypeString:
 		// Anything is a string!
 	default:
-		return diag.Errorf("invalid type %q", typ)
+		return fmt.Errorf("invalid type %q", typ)
 	}
 	return nil
 }
 
-func (v *Parameter) Valid() diag.Diagnostics {
+func (v *Parameter) Valid(input *string, mode ValidationMode) (string, diag.Diagnostics) {
+	if mode != ValidationModeDefault && mode != ValidationModeTemplateImport {
+		return "", diag.Diagnostics{
+			{
+				Severity: diag.Error,
+				Summary:  "Invalid validation mode",
+				Detail:   fmt.Sprintf("validation mode %q is not supported, use %q, or %q", mode, ValidationModeDefault, ValidationModeTemplateImport),
+			},
+		}
+	}
+
+	var err error
+	var optionType OptionType
+
+	value := input
+	if input == nil {
+		value = v.Default
+	}
+
+	// TODO: When empty values want to be rejected, uncomment this.
+	//   coder/coder should update to use template import mode first,
+	//   before this is uncommented.
+	//if value == nil && mode == ValidationModeDefault {
+	//	var empty string
+	//	value = &empty
+	//}
path: provider/parameter.go
user: login: spikecurtis
body: This business wouldn't be necessary if the provider assumes the "looser" validation mode if unset. That is, there are 2 modes: WorkspaceCreate and TemplateImport, which in future we will explicitly set via environment variable. If unset, we assume TemplateImport, which has no breaking changes from current behavior.
created_at: 2025-05-02T06:20:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -429,84 +450,201 @@ func (v *Parameter) Valid() diag.Diagnostics {
 		}
 	}
 
-	optionNames := map[string]any{}
-	optionValues := map[string]any{}
-	if len(v.Option) > 0 {
-		for _, option := range v.Option {
-			_, exists := optionNames[option.Name]
-			if exists {
-				return diag.Diagnostics{{
+	optionValues, diags := v.ValidOptions(optionType, mode)
+	if diags.HasError() {
+		return "", diags
+	}
+
+	if mode == ValidationModeTemplateImport && v.Default != nil {
+		// Template import should validate the default value.
+		err := valueIsType(v.Type, *v.Default)
+		if err != nil {
+			return "", diag.Diagnostics{
+				{
+					Severity:      diag.Error,
+					Summary:       fmt.Sprintf("Default value is not of type %q", v.Type),
+					Detail:        err.Error(),
+					AttributePath: defaultValuePath,
+				},
+			}
+		}
+
+		d := v.validValue(*v.Default, optionType, optionValues, defaultValuePath)
+		if d.HasError() {
+			return "", d
+		}
+	}
+
+	// TODO: This is a bit of a hack. The current behavior states if validation
+	//   is given, then apply validation to unset values.
+	//   This should be removed, and all values should be validated. Meaning
+	//   value == nil should not be accepted in the first place.
+	if len(v.Validation) > 0 && value == nil {
+		empty := ""
+		value = &empty
+	}
+
+	// Value is only validated if it is set. If it is unset, validation
+	// is skipped.
+	if value != nil {
+		d := v.validValue(*value, optionType, optionValues, cty.Path{})
+		if d.HasError() {
+			return "", d
+		}
+
+		err = valueIsType(v.Type, *value)
+		if err != nil {
+			return "", diag.Diagnostics{
+				{
 					Severity: diag.Error,
-					Summary:  "Option names must be unique.",
-					Detail:   fmt.Sprintf("multiple options found with the same name %q", option.Name),
+					Summary:  fmt.Sprintf("Parameter value is not of type %q", v.Type),
+					Detail:   err.Error(),
 				},
-				}
 			}
-			_, exists = optionValues[option.Value]
-			if exists {
-				return diag.Diagnostics{
-					{
-						Severity: diag.Error,
-						Summary:  "Option values must be unique.",
-						Detail:   fmt.Sprintf("multiple options found with the same value %q", option.Value),
-					},
+		}
+	}
+
+	if value == nil {
+		// The previous behavior is to always write an empty string
+		return "", nil
+	}
+
+	return *value, nil
+}
+
+func (v *Parameter) ValidOptions(optionType OptionType, mode ValidationMode) (map[string]struct{}, diag.Diagnostics) {
+	optionNames := map[string]struct{}{}
+	optionValues := map[string]struct{}{}
+
+	var diags diag.Diagnostics
+	for _, option := range v.Option {
+		_, exists := optionNames[option.Name]
+		if exists {
+			return nil, diag.Diagnostics{{
+				Severity: diag.Error,
+				Summary:  "Option names must be unique.",
+				Detail:   fmt.Sprintf("multiple options found with the same name %q", option.Name),
+			}}
+		}
+
+		_, exists = optionValues[option.Value]
+		if exists {
+			return nil, diag.Diagnostics{{
+				Severity: diag.Error,
+				Summary:  "Option values must be unique.",
+				Detail:   fmt.Sprintf("multiple options found with the same value %q", option.Value),
+			}}
+		}
+
+		err := valueIsType(optionType, option.Value)
+		if err != nil {
+			diags = append(diags, diag.Diagnostic{
+				Severity: diag.Error,
+				Summary:  fmt.Sprintf("Option %q with value=%q is not of type %q", option.Name, option.Value, optionType),
+				Detail:   err.Error(),
+			})
+			continue
+		}
+		optionValues[option.Value] = struct{}{}
+		optionNames[option.Name] = struct{}{}
+
+		if mode == ValidationModeTemplateImport {
+			opDiags := v.validValue(option.Value, optionType, nil, cty.Path{})
+			if opDiags.HasError() {
+				for i := range opDiags {
+					opDiags[i].Summary = fmt.Sprintf("Option %q: %s", option.Name, opDiags[i].Summary)
 				}
+				diags = append(diags, opDiags...)
 			}
-			diags := valueIsType(optionType, option.Value, cty.Path{})
-			if diags.HasError() {
-				return diags
-			}
-			optionValues[option.Value] = nil
-			optionNames[option.Name] = nil
 		}
 	}
 
-	if v.Default != "" && len(v.Option) > 0 {
+	if diags.HasError() {
+		return nil, diags
+	}
+	return optionValues, nil
+}
+
+func (v *Parameter) validValue(value string, optionType OptionType, optionValues map[string]struct{}, path cty.Path) diag.Diagnostics {
+	// name is used for constructing more precise error messages.
+	name := "Value"
+	if path.Equals(defaultValuePath) {
+		name = "Default value"
+	}
+
+	// First validate if the value is a valid option
+	if len(optionValues) > 0 {
 		if v.Type == OptionTypeListString && optionType == OptionTypeString {
 			// If the type is list(string) and optionType is string, we have
 			// to ensure all elements of the default exist as options.
-			defaultValues, diags := valueIsListString(v.Default, cty.Path{cty.GetAttrStep{Name: "default"}})
-			if diags.HasError() {
-				return diags
+			listValues, err := valueIsListString(value)
+			if err != nil {
+				return diag.Diagnostics{
+					{
+						Severity:      diag.Error,
+						Summary:       "When using list(string) type, value must be a json encoded list of strings",
+						Detail:        err.Error(),
+						AttributePath: defaultValuePath,
+					},
+				}
 			}
 
 			// missing is used to construct a more helpful error message
 			var missing []string
-			for _, defaultValue := range defaultValues {
-				_, defaultIsValid := optionValues[defaultValue]
-				if !defaultIsValid {
-					missing = append(missing, defaultValue)
+			for _, listValue := range listValues {
+				_, isValid := optionValues[listValue]
+				if !isValid {
+					missing = append(missing, listValue)
 				}
 			}
 
 			if len(missing) > 0 {
 				return diag.Diagnostics{
 					{
 						Severity: diag.Error,
-						Summary:  "Default values must be a valid option",
+						Summary:  fmt.Sprintf("%ss must be a valid option", name),
 						Detail: fmt.Sprintf(
-							"default value %q is not a valid option, values %q are missing from the options",
-							v.Default, strings.Join(missing, ", "),
+							"%s %q is not a valid option, values %q are missing from the options",
+							name, value, strings.Join(missing, ", "),
 						),
-						AttributePath: cty.Path{cty.GetAttrStep{Name: "default"}},
+						AttributePath: defaultValuePath,
 					},
 				}
 			}
 		} else {
-			_, defaultIsValid := optionValues[v.Default]
-			if !defaultIsValid {
+			_, isValid := optionValues[value]
+			if !isValid {
+				extra := ""
+				if value == "" {
+					extra = ". The value is empty, did you forget to set it with a default or from user input?"
+				}
 				return diag.Diagnostics{
 					{
 						Severity:      diag.Error,
-						Summary:       "Default value must be a valid option",
-						Detail:        fmt.Sprintf("the value %q must be defined as one of options", v.Default),
-						AttributePath: cty.Path{cty.GetAttrStep{Name: "default"}},
+						Summary:       fmt.Sprintf("%s must be a valid option%s", name, extra),
+						Detail:        fmt.Sprintf("the value %q must be defined as one of options", value),
+						AttributePath: path,
 					},
 				}
 			}
 		}
 	}
 
+	if len(v.Validation) == 1 {
path: provider/parameter.go
user: login: Emyrk
body: Yes, it is here in the schema: https://github.com/coder/terraform-provider-coder/blob/5ef834e668561694838ee808510ff574b871997d/provider/parameter.go#L289-L289

You get this error:
```
│ Error: Too many validation blocks
│ 
│   on main.tf line 38, in data "coder_parameter" "project":
│   38:   validation {
│ 
│ No more than 1 "validation" blocks are allowed

```
created_at: 2025-05-02T13:33:40Z
author_association: MEMBER
diff_hunk: @@ -429,84 +450,201 @@ func (v *Parameter) Valid() diag.Diagnostics {
 		}
 	}
 
-	optionNames := map[string]any{}
-	optionValues := map[string]any{}
-	if len(v.Option) > 0 {
-		for _, option := range v.Option {
-			_, exists := optionNames[option.Name]
-			if exists {
-				return diag.Diagnostics{{
+	optionValues, diags := v.ValidOptions(optionType, mode)
+	if diags.HasError() {
+		return "", diags
+	}
+
+	if mode == ValidationModeTemplateImport && v.Default != nil {
+		// Template import should validate the default value.
+		err := valueIsType(v.Type, *v.Default)
+		if err != nil {
+			return "", diag.Diagnostics{
+				{
+					Severity:      diag.Error,
+					Summary:       fmt.Sprintf("Default value is not of type %q", v.Type),
+					Detail:        err.Error(),
+					AttributePath: defaultValuePath,
+				},
+			}
+		}
+
+		d := v.validValue(*v.Default, optionType, optionValues, defaultValuePath)
+		if d.HasError() {
+			return "", d
+		}
+	}
+
+	// TODO: This is a bit of a hack. The current behavior states if validation
+	//   is given, then apply validation to unset values.
+	//   This should be removed, and all values should be validated. Meaning
+	//   value == nil should not be accepted in the first place.
+	if len(v.Validation) > 0 && value == nil {
+		empty := ""
+		value = &empty
+	}
path: provider/parameter.go
user: login: Emyrk
body: I can do that :+1: 
created_at: 2025-05-02T13:46:00Z
author_association: MEMBER
diff_hunk: @@ -429,84 +450,201 @@ func (v *Parameter) Valid() diag.Diagnostics {
 		}
 	}
 
-	optionNames := map[string]any{}
-	optionValues := map[string]any{}
-	if len(v.Option) > 0 {
-		for _, option := range v.Option {
-			_, exists := optionNames[option.Name]
-			if exists {
-				return diag.Diagnostics{{
+	optionValues, diags := v.ValidOptions(optionType, mode)
+	if diags.HasError() {
+		return "", diags
+	}
+
+	if mode == ValidationModeTemplateImport && v.Default != nil {
path: provider/parameter.go
user: login: Emyrk
body: You are correct. When we turn this on in coderd, **new** templates imported will be subject to a stricter validation. There is 2 reasons.

## Strict options

You can currently add `option`s to your parameters that are invalid. That is a bit confusing on the UI, when options exist, but if you select them, it would fail to build.

At `create` time, the options are ignored, since they are not used.

## Strict Default

This was actually done before too **iff** no input value was given (which is usually the case for template-import). I made this more explicit, where the `Default` has to be valid if set. Even if you passed in an input value at import time.

I enforce this, because if you do not, then a user who leaves a default value for a form has an invalid value and cannot build.

----

Sidenote, but if you want to place a value in the UI without it being valid, we are adding `styling`:

```terraform
data "coder_parameter" "numbers" {
  name        = "numebrs"
  type        = "number"
  order       = 1

  styling = jsonencode({
    placeholder = "Enter a number"
  })
}
```
created_at: 2025-05-02T14:20:43Z
author_association: MEMBER
diff_hunk: @@ -21,6 +21,25 @@ import (
 	"golang.org/x/xerrors"
 )
 
+type ValidationMode string
+
+const (
+	ValidationModeEnvVar = "CODER_VALIDATION_MODE"
+	// ValidationModeDefault is used for creating a workspace. It validates the final
+	// value used for a parameter. Some allowances for invalid options are tolerated,
+	// as unused options do not affect the final parameter value. The default value
+	// is also ignored, assuming a value is provided.
+	ValidationModeDefault ValidationMode = ""
+	// ValidationModeTemplateImport tolerates empty values, as the value might not be
+	// available at import. It does not tolerate an invalid default or invalid option
path: provider/parameter.go
user: login: Emyrk
body: Correct. I will update
created_at: 2025-05-02T14:22:14Z
author_association: MEMBER
diff_hunk: @@ -389,37 +384,63 @@ func fixValidationResourceData(rawConfig cty.Value, validation interface{}) (int
 	return vArr, nil
 }
 
-func valueIsType(typ OptionType, value string, attrPath cty.Path) diag.Diagnostics {
+func valueIsType(typ OptionType, value string) error {
 	switch typ {
 	case OptionTypeNumber:
 		_, err := strconv.ParseFloat(value, 64)
 		if err != nil {
-			return diag.Errorf("%q is not a number", value)
+			return fmt.Errorf("%q is not a number", value)
 		}
 	case OptionTypeBoolean:
 		_, err := strconv.ParseBool(value)
 		if err != nil {
-			return diag.Errorf("%q is not a bool", value)
+			return fmt.Errorf("%q is not a bool", value)
 		}
 	case OptionTypeListString:
-		_, diags := valueIsListString(value, attrPath)
-		if diags.HasError() {
-			return diags
+		_, err := valueIsListString(value)
+		if err != nil {
+			return err
 		}
 	case OptionTypeString:
 		// Anything is a string!
 	default:
-		return diag.Errorf("invalid type %q", typ)
+		return fmt.Errorf("invalid type %q", typ)
 	}
 	return nil
 }
 
-func (v *Parameter) Valid() diag.Diagnostics {
+func (v *Parameter) Valid(input *string, mode ValidationMode) (string, diag.Diagnostics) {
+	if mode != ValidationModeDefault && mode != ValidationModeTemplateImport {
+		return "", diag.Diagnostics{
+			{
+				Severity: diag.Error,
+				Summary:  "Invalid validation mode",
+				Detail:   fmt.Sprintf("validation mode %q is not supported, use %q, or %q", mode, ValidationModeDefault, ValidationModeTemplateImport),
+			},
+		}
+	}
+
+	var err error
+	var optionType OptionType
+
+	value := input
+	if input == nil {
+		value = v.Default
+	}
+
+	// TODO: When empty values want to be rejected, uncomment this.
+	//   coder/coder should update to use template import mode first,
+	//   before this is uncommented.
+	//if value == nil && mode == ValidationModeDefault {
+	//	var empty string
+	//	value = &empty
+	//}
path: provider/parameter.go
user: login: Emyrk
body: I removed this and did the early `return` instead as you suggested.

`TemplateImport` actually has more breaking changes than `create` in the end. `create` is backwards compatible because imported templates already exist and must still work.

Newly imported templates (once coderd switches), is going to require every `option` to also be valid. By doing this, the workspace create form will only present valid options to the user.


Eventually I would like to disallow an empty input value at `create` entirely.
created_at: 2025-05-02T14:25:45Z
author_association: MEMBER
diff_hunk: @@ -144,38 +138,23 @@ func parameterDataSource() *schema.Resource {
 				return diag.Errorf("ephemeral parameter requires the default property")
 			}
 
-			// TODO: Should we move this into the Valid() function on
-			//  Parameter?
-			if len(parameter.Validation) == 1 {
-				validation := &parameter.Validation[0]
-				err = validation.Valid(parameter.Type, value)
-				if err != nil {
-					return diag.FromErr(err)
-				}
-			}
-
-			// Validate options
-			_, parameter.FormType, err = ValidateFormType(parameter.Type, len(parameter.Option), parameter.FormType)
-			if err != nil {
-				return diag.Diagnostics{
-					{
-						Severity:      diag.Error,
-						Summary:       "Invalid form_type for parameter",
-						Detail:        err.Error(),
-						AttributePath: cty.Path{cty.GetAttrStep{Name: "form_type"}},
-					},
-				}
+			var input *string
+			envValue, ok := os.LookupEnv(ParameterEnvironmentVariable(parameter.Name))
+			if ok {
+				input = &envValue
 			}
-			// Set the form_type back in case the value was changed.
-			// Eg via a default. If a user does not specify, a default value
-			// is used and saved.
-			rd.Set("form_type", parameter.FormType)
 
-			diags := parameter.Valid()
+			value, diags := parameter.ValidateInput(input)
 			if diags.HasError() {
 				return diags
 			}
 
+			// Always set back the value, as it can be sourced from the default
+			rd.Set("value", value)
+
+			// Set the form_type as it could have changed in the validation.
path: provider/parameter.go
user: login: spikecurtis
body: I found this curious and confusing. A fuller explanation is probably worth setting out here: the `form_type` can start off unset, in which case the "validation" code selects a form type based on the `type`.

It's also a bit of a naming failure that we call the procedure that does this "Validation", which implies it doesn't change anything and just renders a verdict. In the Coderd code, I believe we called this procedure "resolution".
created_at: 2025-05-07T07:48:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -429,84 +416,170 @@ func (v *Parameter) Valid() diag.Diagnostics {
 		}
 	}
 
-	optionNames := map[string]any{}
-	optionValues := map[string]any{}
-	if len(v.Option) > 0 {
-		for _, option := range v.Option {
-			_, exists := optionNames[option.Name]
-			if exists {
-				return diag.Diagnostics{{
-					Severity: diag.Error,
-					Summary:  "Option names must be unique.",
-					Detail:   fmt.Sprintf("multiple options found with the same name %q", option.Name),
-				},
-				}
-			}
-			_, exists = optionValues[option.Value]
-			if exists {
-				return diag.Diagnostics{
-					{
-						Severity: diag.Error,
-						Summary:  "Option values must be unique.",
-						Detail:   fmt.Sprintf("multiple options found with the same value %q", option.Value),
-					},
-				}
-			}
-			diags := valueIsType(optionType, option.Value, cty.Path{})
-			if diags.HasError() {
-				return diags
-			}
-			optionValues[option.Value] = nil
-			optionNames[option.Name] = nil
+	optionValues, diags := v.ValidOptions(optionType)
+	if diags.HasError() {
+		return "", diags
+	}
+
+	// TODO: This is a bit of a hack. The current behavior states if validation
+	//   is given, then apply validation to unset values.
+	//   value == nil should not be accepted in the first place.
+	//   To fix this, value should be coerced to an empty string
+	//   if it is nil. Then let the validation logic always apply.
+	if len(v.Validation) == 0 && value == nil {
+		return "", nil
+	}
+
+	// forcedValue ensures the value is not-nil.
+	var forcedValue string
+	if value != nil {
+		forcedValue = *value
+	}
+
+	d := v.validValue(forcedValue, optionType, optionValues, cty.Path{})
+	if d.HasError() {
+		return "", d
+	}
+
+	err = valueIsType(v.Type, forcedValue)
+	if err != nil {
+		return "", diag.Diagnostics{
+			{
+				Severity: diag.Error,
+				Summary:  fmt.Sprintf("Parameter value is not of type %q", v.Type),
+				Detail:   err.Error(),
+			},
 		}
 	}
 
-	if v.Default != "" && len(v.Option) > 0 {
+	return forcedValue, nil
+}
+
+func (v *Parameter) ValidOptions(optionType OptionType) (map[string]struct{}, diag.Diagnostics) {
+	optionNames := map[string]struct{}{}
+	optionValues := map[string]struct{}{}
+
+	var diags diag.Diagnostics
+	for _, option := range v.Option {
+		_, exists := optionNames[option.Name]
+		if exists {
+			return nil, diag.Diagnostics{{
+				Severity: diag.Error,
+				Summary:  "Option names must be unique.",
+				Detail:   fmt.Sprintf("multiple options found with the same name %q", option.Name),
+			}}
+		}
+
+		_, exists = optionValues[option.Value]
+		if exists {
+			return nil, diag.Diagnostics{{
+				Severity: diag.Error,
+				Summary:  "Option values must be unique.",
+				Detail:   fmt.Sprintf("multiple options found with the same value %q", option.Value),
+			}}
+		}
+
+		err := valueIsType(optionType, option.Value)
+		if err != nil {
+			diags = append(diags, diag.Diagnostic{
+				Severity: diag.Error,
+				Summary:  fmt.Sprintf("Option %q with value=%q is not of type %q", option.Name, option.Value, optionType),
+				Detail:   err.Error(),
+			})
+			continue
+		}
+		optionValues[option.Value] = struct{}{}
+		optionNames[option.Name] = struct{}{}
+
+		// Option values are assumed to be valid. Do not call validValue on them.
+	}
+
+	if diags != nil && diags.HasError() {
+		return nil, diags
+	}
+	return optionValues, nil
+}
+
+func (v *Parameter) validValue(value string, optionType OptionType, optionValues map[string]struct{}, path cty.Path) diag.Diagnostics {
+	// name is used for constructing more precise error messages.
+	name := "Value"
+	if path.Equals(defaultValuePath) {
+		name = "Default value"
+	}
+
+	// First validate if the value is a valid option
+	if len(optionValues) > 0 {
 		if v.Type == OptionTypeListString && optionType == OptionTypeString {
 			// If the type is list(string) and optionType is string, we have
 			// to ensure all elements of the default exist as options.
-			defaultValues, diags := valueIsListString(v.Default, cty.Path{cty.GetAttrStep{Name: "default"}})
-			if diags.HasError() {
-				return diags
+			listValues, err := valueIsListString(value)
path: provider/parameter.go
user: login: spikecurtis
body: I think the comment above is out of date, as we are not specifically checking the default anymore
created_at: 2025-05-07T07:54:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -429,84 +416,170 @@ func (v *Parameter) Valid() diag.Diagnostics {
 		}
 	}
 
-	optionNames := map[string]any{}
-	optionValues := map[string]any{}
-	if len(v.Option) > 0 {
-		for _, option := range v.Option {
-			_, exists := optionNames[option.Name]
-			if exists {
-				return diag.Diagnostics{{
-					Severity: diag.Error,
-					Summary:  "Option names must be unique.",
-					Detail:   fmt.Sprintf("multiple options found with the same name %q", option.Name),
-				},
-				}
-			}
-			_, exists = optionValues[option.Value]
-			if exists {
-				return diag.Diagnostics{
-					{
-						Severity: diag.Error,
-						Summary:  "Option values must be unique.",
-						Detail:   fmt.Sprintf("multiple options found with the same value %q", option.Value),
-					},
-				}
-			}
-			diags := valueIsType(optionType, option.Value, cty.Path{})
-			if diags.HasError() {
-				return diags
-			}
-			optionValues[option.Value] = nil
-			optionNames[option.Name] = nil
+	optionValues, diags := v.ValidOptions(optionType)
+	if diags.HasError() {
+		return "", diags
+	}
+
+	// TODO: This is a bit of a hack. The current behavior states if validation
+	//   is given, then apply validation to unset values.
+	//   value == nil should not be accepted in the first place.
+	//   To fix this, value should be coerced to an empty string
+	//   if it is nil. Then let the validation logic always apply.
+	if len(v.Validation) == 0 && value == nil {
+		return "", nil
+	}
+
+	// forcedValue ensures the value is not-nil.
+	var forcedValue string
+	if value != nil {
+		forcedValue = *value
+	}
+
+	d := v.validValue(forcedValue, optionType, optionValues, cty.Path{})
+	if d.HasError() {
+		return "", d
+	}
+
+	err = valueIsType(v.Type, forcedValue)
+	if err != nil {
+		return "", diag.Diagnostics{
+			{
+				Severity: diag.Error,
+				Summary:  fmt.Sprintf("Parameter value is not of type %q", v.Type),
+				Detail:   err.Error(),
+			},
 		}
 	}
 
-	if v.Default != "" && len(v.Option) > 0 {
+	return forcedValue, nil
+}
+
+func (v *Parameter) ValidOptions(optionType OptionType) (map[string]struct{}, diag.Diagnostics) {
+	optionNames := map[string]struct{}{}
+	optionValues := map[string]struct{}{}
+
+	var diags diag.Diagnostics
+	for _, option := range v.Option {
+		_, exists := optionNames[option.Name]
+		if exists {
+			return nil, diag.Diagnostics{{
+				Severity: diag.Error,
+				Summary:  "Option names must be unique.",
+				Detail:   fmt.Sprintf("multiple options found with the same name %q", option.Name),
+			}}
+		}
+
+		_, exists = optionValues[option.Value]
+		if exists {
+			return nil, diag.Diagnostics{{
+				Severity: diag.Error,
+				Summary:  "Option values must be unique.",
+				Detail:   fmt.Sprintf("multiple options found with the same value %q", option.Value),
+			}}
+		}
+
+		err := valueIsType(optionType, option.Value)
+		if err != nil {
+			diags = append(diags, diag.Diagnostic{
+				Severity: diag.Error,
+				Summary:  fmt.Sprintf("Option %q with value=%q is not of type %q", option.Name, option.Value, optionType),
+				Detail:   err.Error(),
+			})
+			continue
+		}
+		optionValues[option.Value] = struct{}{}
+		optionNames[option.Name] = struct{}{}
+
+		// Option values are assumed to be valid. Do not call validValue on them.
+	}
+
+	if diags != nil && diags.HasError() {
+		return nil, diags
+	}
+	return optionValues, nil
+}
+
+func (v *Parameter) validValue(value string, optionType OptionType, optionValues map[string]struct{}, path cty.Path) diag.Diagnostics {
path: provider/parameter.go
user: login: spikecurtis
body: this function is called only once, with an empty path.
created_at: 2025-05-07T07:56:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -429,84 +416,170 @@ func (v *Parameter) Valid() diag.Diagnostics {
 		}
 	}
 
-	optionNames := map[string]any{}
-	optionValues := map[string]any{}
-	if len(v.Option) > 0 {
-		for _, option := range v.Option {
-			_, exists := optionNames[option.Name]
-			if exists {
-				return diag.Diagnostics{{
-					Severity: diag.Error,
-					Summary:  "Option names must be unique.",
-					Detail:   fmt.Sprintf("multiple options found with the same name %q", option.Name),
-				},
-				}
-			}
-			_, exists = optionValues[option.Value]
-			if exists {
-				return diag.Diagnostics{
-					{
-						Severity: diag.Error,
-						Summary:  "Option values must be unique.",
-						Detail:   fmt.Sprintf("multiple options found with the same value %q", option.Value),
-					},
-				}
-			}
-			diags := valueIsType(optionType, option.Value, cty.Path{})
-			if diags.HasError() {
-				return diags
-			}
-			optionValues[option.Value] = nil
-			optionNames[option.Name] = nil
+	optionValues, diags := v.ValidOptions(optionType)
+	if diags.HasError() {
+		return "", diags
+	}
+
+	// TODO: This is a bit of a hack. The current behavior states if validation
+	//   is given, then apply validation to unset values.
+	//   value == nil should not be accepted in the first place.
+	//   To fix this, value should be coerced to an empty string
+	//   if it is nil. Then let the validation logic always apply.
+	if len(v.Validation) == 0 && value == nil {
+		return "", nil
+	}
+
+	// forcedValue ensures the value is not-nil.
+	var forcedValue string
+	if value != nil {
+		forcedValue = *value
+	}
+
+	d := v.validValue(forcedValue, optionType, optionValues, cty.Path{})
+	if d.HasError() {
+		return "", d
+	}
+
+	err = valueIsType(v.Type, forcedValue)
+	if err != nil {
+		return "", diag.Diagnostics{
+			{
+				Severity: diag.Error,
+				Summary:  fmt.Sprintf("Parameter value is not of type %q", v.Type),
+				Detail:   err.Error(),
+			},
 		}
 	}
 
-	if v.Default != "" && len(v.Option) > 0 {
+	return forcedValue, nil
+}
+
+func (v *Parameter) ValidOptions(optionType OptionType) (map[string]struct{}, diag.Diagnostics) {
+	optionNames := map[string]struct{}{}
+	optionValues := map[string]struct{}{}
+
+	var diags diag.Diagnostics
+	for _, option := range v.Option {
+		_, exists := optionNames[option.Name]
+		if exists {
+			return nil, diag.Diagnostics{{
+				Severity: diag.Error,
+				Summary:  "Option names must be unique.",
+				Detail:   fmt.Sprintf("multiple options found with the same name %q", option.Name),
+			}}
+		}
+
+		_, exists = optionValues[option.Value]
+		if exists {
+			return nil, diag.Diagnostics{{
+				Severity: diag.Error,
+				Summary:  "Option values must be unique.",
+				Detail:   fmt.Sprintf("multiple options found with the same value %q", option.Value),
+			}}
+		}
+
+		err := valueIsType(optionType, option.Value)
+		if err != nil {
+			diags = append(diags, diag.Diagnostic{
+				Severity: diag.Error,
+				Summary:  fmt.Sprintf("Option %q with value=%q is not of type %q", option.Name, option.Value, optionType),
+				Detail:   err.Error(),
+			})
+			continue
+		}
+		optionValues[option.Value] = struct{}{}
+		optionNames[option.Name] = struct{}{}
+
+		// Option values are assumed to be valid. Do not call validValue on them.
+	}
+
+	if diags != nil && diags.HasError() {
+		return nil, diags
+	}
+	return optionValues, nil
+}
+
+func (v *Parameter) validValue(value string, optionType OptionType, optionValues map[string]struct{}, path cty.Path) diag.Diagnostics {
+	// name is used for constructing more precise error messages.
+	name := "Value"
+	if path.Equals(defaultValuePath) {
+		name = "Default value"
+	}
+
+	// First validate if the value is a valid option
+	if len(optionValues) > 0 {
 		if v.Type == OptionTypeListString && optionType == OptionTypeString {
 			// If the type is list(string) and optionType is string, we have
 			// to ensure all elements of the default exist as options.
-			defaultValues, diags := valueIsListString(v.Default, cty.Path{cty.GetAttrStep{Name: "default"}})
-			if diags.HasError() {
-				return diags
+			listValues, err := valueIsListString(value)
+			if err != nil {
+				return diag.Diagnostics{
+					{
+						Severity:      diag.Error,
+						Summary:       "When using list(string) type, value must be a json encoded list of strings",
+						Detail:        err.Error(),
+						AttributePath: defaultValuePath,
path: provider/parameter.go
user: login: spikecurtis
body: We don't specifically know this is the default; it could just be the normal input.
created_at: 2025-05-07T07:57:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -429,84 +416,170 @@ func (v *Parameter) Valid() diag.Diagnostics {
 		}
 	}
 
-	optionNames := map[string]any{}
-	optionValues := map[string]any{}
-	if len(v.Option) > 0 {
-		for _, option := range v.Option {
-			_, exists := optionNames[option.Name]
-			if exists {
-				return diag.Diagnostics{{
-					Severity: diag.Error,
-					Summary:  "Option names must be unique.",
-					Detail:   fmt.Sprintf("multiple options found with the same name %q", option.Name),
-				},
-				}
-			}
-			_, exists = optionValues[option.Value]
-			if exists {
-				return diag.Diagnostics{
-					{
-						Severity: diag.Error,
-						Summary:  "Option values must be unique.",
-						Detail:   fmt.Sprintf("multiple options found with the same value %q", option.Value),
-					},
-				}
-			}
-			diags := valueIsType(optionType, option.Value, cty.Path{})
-			if diags.HasError() {
-				return diags
-			}
-			optionValues[option.Value] = nil
-			optionNames[option.Name] = nil
+	optionValues, diags := v.ValidOptions(optionType)
+	if diags.HasError() {
+		return "", diags
+	}
+
+	// TODO: This is a bit of a hack. The current behavior states if validation
+	//   is given, then apply validation to unset values.
+	//   value == nil should not be accepted in the first place.
+	//   To fix this, value should be coerced to an empty string
+	//   if it is nil. Then let the validation logic always apply.
+	if len(v.Validation) == 0 && value == nil {
+		return "", nil
+	}
+
+	// forcedValue ensures the value is not-nil.
+	var forcedValue string
+	if value != nil {
+		forcedValue = *value
+	}
+
+	d := v.validValue(forcedValue, optionType, optionValues, cty.Path{})
+	if d.HasError() {
+		return "", d
+	}
+
+	err = valueIsType(v.Type, forcedValue)
+	if err != nil {
+		return "", diag.Diagnostics{
+			{
+				Severity: diag.Error,
+				Summary:  fmt.Sprintf("Parameter value is not of type %q", v.Type),
+				Detail:   err.Error(),
+			},
 		}
 	}
 
-	if v.Default != "" && len(v.Option) > 0 {
+	return forcedValue, nil
+}
+
+func (v *Parameter) ValidOptions(optionType OptionType) (map[string]struct{}, diag.Diagnostics) {
+	optionNames := map[string]struct{}{}
+	optionValues := map[string]struct{}{}
+
+	var diags diag.Diagnostics
+	for _, option := range v.Option {
+		_, exists := optionNames[option.Name]
+		if exists {
+			return nil, diag.Diagnostics{{
+				Severity: diag.Error,
+				Summary:  "Option names must be unique.",
+				Detail:   fmt.Sprintf("multiple options found with the same name %q", option.Name),
+			}}
+		}
+
+		_, exists = optionValues[option.Value]
+		if exists {
+			return nil, diag.Diagnostics{{
+				Severity: diag.Error,
+				Summary:  "Option values must be unique.",
+				Detail:   fmt.Sprintf("multiple options found with the same value %q", option.Value),
+			}}
+		}
+
+		err := valueIsType(optionType, option.Value)
+		if err != nil {
+			diags = append(diags, diag.Diagnostic{
+				Severity: diag.Error,
+				Summary:  fmt.Sprintf("Option %q with value=%q is not of type %q", option.Name, option.Value, optionType),
+				Detail:   err.Error(),
+			})
+			continue
+		}
+		optionValues[option.Value] = struct{}{}
+		optionNames[option.Name] = struct{}{}
+
+		// Option values are assumed to be valid. Do not call validValue on them.
+	}
+
+	if diags != nil && diags.HasError() {
+		return nil, diags
+	}
+	return optionValues, nil
+}
+
+func (v *Parameter) validValue(value string, optionType OptionType, optionValues map[string]struct{}, path cty.Path) diag.Diagnostics {
+	// name is used for constructing more precise error messages.
+	name := "Value"
+	if path.Equals(defaultValuePath) {
+		name = "Default value"
+	}
+
+	// First validate if the value is a valid option
+	if len(optionValues) > 0 {
 		if v.Type == OptionTypeListString && optionType == OptionTypeString {
 			// If the type is list(string) and optionType is string, we have
 			// to ensure all elements of the default exist as options.
-			defaultValues, diags := valueIsListString(v.Default, cty.Path{cty.GetAttrStep{Name: "default"}})
-			if diags.HasError() {
-				return diags
+			listValues, err := valueIsListString(value)
+			if err != nil {
+				return diag.Diagnostics{
+					{
+						Severity:      diag.Error,
+						Summary:       "When using list(string) type, value must be a json encoded list of strings",
+						Detail:        err.Error(),
+						AttributePath: defaultValuePath,
+					},
+				}
 			}
 
 			// missing is used to construct a more helpful error message
 			var missing []string
-			for _, defaultValue := range defaultValues {
-				_, defaultIsValid := optionValues[defaultValue]
-				if !defaultIsValid {
-					missing = append(missing, defaultValue)
+			for _, listValue := range listValues {
+				_, isValid := optionValues[listValue]
+				if !isValid {
+					missing = append(missing, listValue)
 				}
 			}
 
 			if len(missing) > 0 {
 				return diag.Diagnostics{
 					{
 						Severity: diag.Error,
-						Summary:  "Default values must be a valid option",
+						Summary:  fmt.Sprintf("%ss must be a valid option", name),
 						Detail: fmt.Sprintf(
-							"default value %q is not a valid option, values %q are missing from the options",
-							v.Default, strings.Join(missing, ", "),
+							"%s %q is not a valid option, values %q are missing from the options",
+							name, value, strings.Join(missing, ", "),
 						),
-						AttributePath: cty.Path{cty.GetAttrStep{Name: "default"}},
+						AttributePath: defaultValuePath,
path: provider/parameter.go
user: login: spikecurtis
body: Again, this might not be the default.
created_at: 2025-05-07T07:57:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -144,38 +138,23 @@ func parameterDataSource() *schema.Resource {
 				return diag.Errorf("ephemeral parameter requires the default property")
 			}
 
-			// TODO: Should we move this into the Valid() function on
-			//  Parameter?
-			if len(parameter.Validation) == 1 {
-				validation := &parameter.Validation[0]
-				err = validation.Valid(parameter.Type, value)
-				if err != nil {
-					return diag.FromErr(err)
-				}
-			}
-
-			// Validate options
-			_, parameter.FormType, err = ValidateFormType(parameter.Type, len(parameter.Option), parameter.FormType)
-			if err != nil {
-				return diag.Diagnostics{
-					{
-						Severity:      diag.Error,
-						Summary:       "Invalid form_type for parameter",
-						Detail:        err.Error(),
-						AttributePath: cty.Path{cty.GetAttrStep{Name: "form_type"}},
-					},
-				}
+			var input *string
+			envValue, ok := os.LookupEnv(ParameterEnvironmentVariable(parameter.Name))
+			if ok {
+				input = &envValue
 			}
-			// Set the form_type back in case the value was changed.
-			// Eg via a default. If a user does not specify, a default value
-			// is used and saved.
-			rd.Set("form_type", parameter.FormType)
 
-			diags := parameter.Valid()
+			value, diags := parameter.ValidateInput(input)
 			if diags.HasError() {
 				return diags
 			}
 
+			// Always set back the value, as it can be sourced from the default
+			rd.Set("value", value)
+
+			// Set the form_type as it could have changed in the validation.
path: provider/parameter.go
user: login: Emyrk
body: It is unfortunate. I updated the comment.
created_at: 2025-05-07T12:46:52Z
author_association: MEMBER
diff_hunk: @@ -429,84 +416,170 @@ func (v *Parameter) Valid() diag.Diagnostics {
 		}
 	}
 
-	optionNames := map[string]any{}
-	optionValues := map[string]any{}
-	if len(v.Option) > 0 {
-		for _, option := range v.Option {
-			_, exists := optionNames[option.Name]
-			if exists {
-				return diag.Diagnostics{{
-					Severity: diag.Error,
-					Summary:  "Option names must be unique.",
-					Detail:   fmt.Sprintf("multiple options found with the same name %q", option.Name),
-				},
-				}
-			}
-			_, exists = optionValues[option.Value]
-			if exists {
-				return diag.Diagnostics{
-					{
-						Severity: diag.Error,
-						Summary:  "Option values must be unique.",
-						Detail:   fmt.Sprintf("multiple options found with the same value %q", option.Value),
-					},
-				}
-			}
-			diags := valueIsType(optionType, option.Value, cty.Path{})
-			if diags.HasError() {
-				return diags
-			}
-			optionValues[option.Value] = nil
-			optionNames[option.Name] = nil
+	optionValues, diags := v.ValidOptions(optionType)
+	if diags.HasError() {
+		return "", diags
+	}
+
+	// TODO: This is a bit of a hack. The current behavior states if validation
+	//   is given, then apply validation to unset values.
+	//   value == nil should not be accepted in the first place.
+	//   To fix this, value should be coerced to an empty string
+	//   if it is nil. Then let the validation logic always apply.
+	if len(v.Validation) == 0 && value == nil {
+		return "", nil
+	}
+
+	// forcedValue ensures the value is not-nil.
+	var forcedValue string
+	if value != nil {
+		forcedValue = *value
+	}
+
+	d := v.validValue(forcedValue, optionType, optionValues, cty.Path{})
+	if d.HasError() {
+		return "", d
+	}
+
+	err = valueIsType(v.Type, forcedValue)
+	if err != nil {
+		return "", diag.Diagnostics{
+			{
+				Severity: diag.Error,
+				Summary:  fmt.Sprintf("Parameter value is not of type %q", v.Type),
+				Detail:   err.Error(),
+			},
 		}
 	}
 
-	if v.Default != "" && len(v.Option) > 0 {
+	return forcedValue, nil
+}
+
+func (v *Parameter) ValidOptions(optionType OptionType) (map[string]struct{}, diag.Diagnostics) {
+	optionNames := map[string]struct{}{}
+	optionValues := map[string]struct{}{}
+
+	var diags diag.Diagnostics
+	for _, option := range v.Option {
+		_, exists := optionNames[option.Name]
+		if exists {
+			return nil, diag.Diagnostics{{
+				Severity: diag.Error,
+				Summary:  "Option names must be unique.",
+				Detail:   fmt.Sprintf("multiple options found with the same name %q", option.Name),
+			}}
+		}
+
+		_, exists = optionValues[option.Value]
+		if exists {
+			return nil, diag.Diagnostics{{
+				Severity: diag.Error,
+				Summary:  "Option values must be unique.",
+				Detail:   fmt.Sprintf("multiple options found with the same value %q", option.Value),
+			}}
+		}
+
+		err := valueIsType(optionType, option.Value)
+		if err != nil {
+			diags = append(diags, diag.Diagnostic{
+				Severity: diag.Error,
+				Summary:  fmt.Sprintf("Option %q with value=%q is not of type %q", option.Name, option.Value, optionType),
+				Detail:   err.Error(),
+			})
+			continue
+		}
+		optionValues[option.Value] = struct{}{}
+		optionNames[option.Name] = struct{}{}
+
+		// Option values are assumed to be valid. Do not call validValue on them.
+	}
+
+	if diags != nil && diags.HasError() {
+		return nil, diags
+	}
+	return optionValues, nil
+}
+
+func (v *Parameter) validValue(value string, optionType OptionType, optionValues map[string]struct{}, path cty.Path) diag.Diagnostics {
path: provider/parameter.go
user: login: Emyrk
body: Updated the call to pass in the default value path if the value is sourced from the default.

The path (if it can be specified), makes the error a bit nicer:

![Screenshot From 2025-05-07 07-50-11](https://github.com/user-attachments/assets/638cb44f-903c-4b0e-9f67-6edbd607aa0b)


Without path

![Screenshot From 2025-05-07 07-50-16](https://github.com/user-attachments/assets/15bce8a7-88f8-44ab-a3f8-445e5149c6c7)

created_at: 2025-05-07T12:50:30Z
author_association: MEMBER
diff_hunk: @@ -429,84 +416,170 @@ func (v *Parameter) Valid() diag.Diagnostics {
 		}
 	}
 
-	optionNames := map[string]any{}
-	optionValues := map[string]any{}
-	if len(v.Option) > 0 {
-		for _, option := range v.Option {
-			_, exists := optionNames[option.Name]
-			if exists {
-				return diag.Diagnostics{{
-					Severity: diag.Error,
-					Summary:  "Option names must be unique.",
-					Detail:   fmt.Sprintf("multiple options found with the same name %q", option.Name),
-				},
-				}
-			}
-			_, exists = optionValues[option.Value]
-			if exists {
-				return diag.Diagnostics{
-					{
-						Severity: diag.Error,
-						Summary:  "Option values must be unique.",
-						Detail:   fmt.Sprintf("multiple options found with the same value %q", option.Value),
-					},
-				}
-			}
-			diags := valueIsType(optionType, option.Value, cty.Path{})
-			if diags.HasError() {
-				return diags
-			}
-			optionValues[option.Value] = nil
-			optionNames[option.Name] = nil
+	optionValues, diags := v.ValidOptions(optionType)
+	if diags.HasError() {
+		return "", diags
+	}
+
+	// TODO: This is a bit of a hack. The current behavior states if validation
+	//   is given, then apply validation to unset values.
+	//   value == nil should not be accepted in the first place.
+	//   To fix this, value should be coerced to an empty string
+	//   if it is nil. Then let the validation logic always apply.
+	if len(v.Validation) == 0 && value == nil {
+		return "", nil
+	}
+
+	// forcedValue ensures the value is not-nil.
+	var forcedValue string
+	if value != nil {
+		forcedValue = *value
+	}
+
+	d := v.validValue(forcedValue, optionType, optionValues, cty.Path{})
+	if d.HasError() {
+		return "", d
+	}
+
+	err = valueIsType(v.Type, forcedValue)
+	if err != nil {
+		return "", diag.Diagnostics{
+			{
+				Severity: diag.Error,
+				Summary:  fmt.Sprintf("Parameter value is not of type %q", v.Type),
+				Detail:   err.Error(),
+			},
 		}
 	}
 
-	if v.Default != "" && len(v.Option) > 0 {
+	return forcedValue, nil
+}
+
+func (v *Parameter) ValidOptions(optionType OptionType) (map[string]struct{}, diag.Diagnostics) {
+	optionNames := map[string]struct{}{}
+	optionValues := map[string]struct{}{}
+
+	var diags diag.Diagnostics
+	for _, option := range v.Option {
+		_, exists := optionNames[option.Name]
+		if exists {
+			return nil, diag.Diagnostics{{
+				Severity: diag.Error,
+				Summary:  "Option names must be unique.",
+				Detail:   fmt.Sprintf("multiple options found with the same name %q", option.Name),
+			}}
+		}
+
+		_, exists = optionValues[option.Value]
+		if exists {
+			return nil, diag.Diagnostics{{
+				Severity: diag.Error,
+				Summary:  "Option values must be unique.",
+				Detail:   fmt.Sprintf("multiple options found with the same value %q", option.Value),
+			}}
+		}
+
+		err := valueIsType(optionType, option.Value)
+		if err != nil {
+			diags = append(diags, diag.Diagnostic{
+				Severity: diag.Error,
+				Summary:  fmt.Sprintf("Option %q with value=%q is not of type %q", option.Name, option.Value, optionType),
+				Detail:   err.Error(),
+			})
+			continue
+		}
+		optionValues[option.Value] = struct{}{}
+		optionNames[option.Name] = struct{}{}
+
+		// Option values are assumed to be valid. Do not call validValue on them.
+	}
+
+	if diags != nil && diags.HasError() {
+		return nil, diags
+	}
+	return optionValues, nil
+}
+
+func (v *Parameter) validValue(value string, optionType OptionType, optionValues map[string]struct{}, path cty.Path) diag.Diagnostics {
+	// name is used for constructing more precise error messages.
+	name := "Value"
+	if path.Equals(defaultValuePath) {
+		name = "Default value"
+	}
+
+	// First validate if the value is a valid option
+	if len(optionValues) > 0 {
 		if v.Type == OptionTypeListString && optionType == OptionTypeString {
 			// If the type is list(string) and optionType is string, we have
 			// to ensure all elements of the default exist as options.
-			defaultValues, diags := valueIsListString(v.Default, cty.Path{cty.GetAttrStep{Name: "default"}})
-			if diags.HasError() {
-				return diags
+			listValues, err := valueIsListString(value)
+			if err != nil {
+				return diag.Diagnostics{
+					{
+						Severity:      diag.Error,
+						Summary:       "When using list(string) type, value must be a json encoded list of strings",
+						Detail:        err.Error(),
+						AttributePath: defaultValuePath,
path: provider/parameter.go
user: login: Emyrk
body: Good catch. It should all be `path`
created_at: 2025-05-07T12:51:36Z
author_association: MEMBER

PR comments:
user: login: Emyrk
created_at: 2025-04-14T22:24:09Z
body: @johnstcn @matifali should this pass a template import?

Before this PR, this would pass a `terraform plan`.

```hcl
data "coder_parameter" "region" {
  name        = "region"
  description = "Which region would you like to deploy to?"
  type        = "string"
  order       = 1

  option {
    name  = "Europe"
    value = "eu"
  }
  option {
    name  = "United States"
    value = "us"
  }
}
```

EDIT: Test case here https://github.com/coder/terraform-provider-coder/blob/2d51bff8064a2b44b087fcadae36ce937d2cd2d8/provider/parameter_test.go#L712-L712

This PR sets the error to:

```
Planning failed. Terraform encountered an error while generating this plan.

╷
│ Error: Value must be a valid option. The value is empty, did you forget to set it with a default or from user input?
│ 
│   with data.coder_parameter.region,
│   on main.tf line 21, in data "coder_parameter" "region":
│   21: data "coder_parameter" "region" {
│ 
│ the value "" must be defined as one of options
```

To fix this, either the value must be passed via an env var:

```
CODER_PARAMETER_c697d2981bf416569a16cfbcdec1542b5398f3cc77d2b905819aa99c46ecf6f6=us terraform plan
```

Or with a default:

```
data "coder_parameter" "region" {
  # ...
  default = "us"
  # ...
}
```

--------

I feel like we should reject it? But that is a breaking change.

author_association: MEMBER
user: login: matifali
created_at: 2025-04-14T22:33:52Z
body: There are use cases where an admin may not want to set a default to force the user to choose an option. At least it was possible before this PR. I am not sure how many users were actually using it. 

But I have seen requests to support `coder_parameter` without a default value better: https://github.com/coder/terraform-provider-coder/issues/144
author_association: MEMBER
user: login: johnstcn
created_at: 2025-04-15T10:31:20Z
body: > There are use cases where an admin may not want to set a default to force the user to choose an option. 

This behaviour is problematic because you could end up with a plan that does not correspond to the apply, depending on how the value of the parameter is used.

Would marking the parameter as `ephemeral` be a possible workaround here?

EDIT: marking a parameter as ephemeral requires it to be mutable and to also have a default set. This may not suit certain use-cases.
author_association: MEMBER
user: login: matifali
created_at: 2025-04-15T10:35:42Z
body: Do we require a user input if the parameters are ephemeral? Do we not allow setting a default value for such parameters?
Do such ephemeral parameters support options too?

If the answers are yes, then we just need better docs on how to fulfill this use case. 

So that people don't try to use the existing non ephemeral parameters. 
author_association: MEMBER
user: login: Emyrk
created_at: 2025-04-29T14:19:13Z
body: @matifali @johnstcn 

How I see it is the relaxed validation on template import is incorrect. If a parameter has 0 values, but will have some value at workspace create, then the `terraform plan` could be incorrect.

However, I understand the use case. The provider SDK we are using does not indicate anywhere if `terraform plan` vs `terraform apply` is being run. So there is no way to conditionally enforce validation that way.

My gut thought is that we need to effectively have 2 modes of validation. 1 for template import, 1 for workspace create, where the latter is strict. The strict validation is the most correct.

What if we pass through an env var a relaxed validation mode? `coder/coder` can pass `CODER_VALIDATION=relaxed` when doing a template import. Then the provider can accept `null` values.
author_association: MEMBER
user: login: johnstcn
created_at: 2025-04-29T14:22:40Z
body: > What if we pass through an env var a relaxed validation mode? `coder/coder` can pass `CODER_VALIDATION=relaxed` when doing a template import. Then the provider can accept `null` values.

I think this approach could work, but we'd want to perhaps tie its behaviour specifically to what "operation" we're doing in Coder. We might also want to be wary of folks just setting this env var on the provisioner to work around null values entirely.
author_association: MEMBER
user: login: Emyrk
created_at: 2025-04-30T13:57:14Z
body: > I think this approach could work, but we'd want to perhaps tie its behaviour specifically to what "operation" we're doing in Coder. We might also want to be wary of folks just setting this env var on the provisioner to work around null values entirely.

Yes, template import would have to pass some env var to change the behavior.

As for folks setting this env var at home, I think that would just be on them. By default the validation should be handled correctly in coder (when to be relaxed, and when not)
author_association: MEMBER
user: login: spikecurtis
created_at: 2025-05-02T05:43:36Z
body: > As for folks setting this env var at home, I think that would just be on them. By default the validation should be handled correctly in coder (when to be relaxed, and when not)

If provisionerd _always_ sets the environment variable before exec'ing terraform, then I don't think we need to worry about users setting it.
author_association: CONTRIBUTOR
user: login: Emyrk
created_at: 2025-05-02T13:43:42Z
body: > It seems very odd to me that in this PR, template-import mode, which is supposedly being created for back-compatibility is more strict than what you're calling default mode.

> Shouldn't it be the other way around? That template-import mode skips some checks that would break backward compatibility?

Maybe I should have broken this into 2 PRs. My goal is to make existing imported templates backwards compatible. They will function as they used to.

Future imported templates I am closing some of the validation holes.

And the backwards _feature_ I am keeping, is allowing empty an `default` value. So this is **still** valid:

```terraform
data "coder_parameter" "region" {
  name        = "region"
  description = "Which region would you like to deploy to?"
  type        = "string"
  order       = 1

  option {
    name  = "Europe"
    value = "eu"
  }
}
```

The other changes I made are fixing validation bugs. Meaning validation used to be skipped on present values.

---


## Validating input values is in the option set

This is in `coder/coder`. This change shifts some of the validation in `coder/coder` -> `terraform-provider-coder`.

This is now invalid except at `template-import`, since no input values are given. So at `template-import`, there is no change in behavior here.
```terraform
# Using input value 3
# CODER_PARAMETER_f3c7807d475073ba009bf4801b2d934e9f0126cb96dd19a27dbffcae23a7f5a3=3 terraform plan

data "coder_parameter" "numbers" {
  name        = "numebrs"
  type        = "number"
  order       = 1

  option {
    name  = "Five"
    value = "5"
  }
}
```

## Validating option values

This used to pass, and it will continue to pass `create` so that previous templates still build if they have been imported. I cannot break existing imported templates.

For future `template-import`s, I throw an error. I think this fix can be seen as a bug fix. Otherwise, dropdown options in the UI form will fail when you select them.

```terraform
data "coder_parameter" "numbers" {
  name        = "numebrs"
  type        = "number"
  order       = 1
  default = 7

  validation {
    min = 6
    max = 10
    error = "The number must be between 6 and 10"
  }

  option {
    name  = "Seven"
    value = "7"
  }

  option {
    name  = "Four"
    value = "4"
  }
}
```

```
│ Error: Option "Four": Invalid parameter value according to 'validation' block
│ 
│   with data.coder_parameter.numbers,
│   on main.tf line 10, in data "coder_parameter" "numbers":
│   10: data "coder_parameter" "numbers" {
│ 
│ The number must be between 6 and 10
```


author_association: MEMBER
user: login: Emyrk
created_at: 2025-05-02T13:44:16Z
body: > If provisionerd _always_ sets the environment variable before exec'ing terraform, then I don't think we need to worry about users setting it.

I agree, I was asked this in a voice chat. It can be set by users, but I don't imagine they will.
author_association: MEMBER
user: login: Emyrk
created_at: 2025-05-05T16:27:10Z
body: I removed the idea of 2 validation modes. There is only 1. `option` values are not validated, just as they were previously skipped.

The new validation mode is backwards compatible in all intended behavior. 
author_association: MEMBER