The framework can return diagnostics feedback for values in provider, resource, and data source configurations. This allows you to write validations that give users feedback about required syntax, types, and acceptable values.
Note: When implementing validation logic, configuration values may be unknown based on the source of the value. Implementations must account for this case, typically by returning early without returning new diagnostics.
The Terraform configuration language is declarative and an implementation of HashiCorp Configuration Language (HCL). The Terraform CLI is responsible for reading and parsing configurations for validity, based on Terraform's concepts such as resource blocks and associated syntax. The Terraform CLI automatically handles basic validation of value type and behavior information based on the provider, resource, or data source schema. For example, the Terraform CLI returns an error when a string value is given where a list value is expected and also when a required attribute is missing from a configuration.
You can introduce validation on attributes using the generic framework-defined types such as types.String. To do this, supply the tfsdk.Attribute type Validators field with a list of validations, and the framework will return diagnostics from all validators. For example:
// Typically within the tfsdk.Schema returned by GetSchema() for a provider,// resource, or data source.
tfsdk.Attribute{// ... other Attribute configuration ...
Validators:[]AttributeValidators{// These are example validators from terraform-plugin-framework-validators
stringvalidator.LengthBetween(10,256),
stringvalidator.RegexMatches(
regexp.MustCompile(`^[a-z0-9]+$`),"must contain only lowercase alphanumeric characters",),},}
// Typically within the tfsdk.Schema returned by GetSchema() for a provider,// resource, or data source.tfsdk.Attribute{// ... other Attribute configuration ... Validators:[]AttributeValidators{// These are example validators from terraform-plugin-framework-validators stringvalidator.LengthBetween(10,256), stringvalidator.RegexMatches( regexp.MustCompile(`^[a-z0-9]+$`),"must contain only lowercase alphanumeric characters",),},}
All validators in the slice will always be run, regardless of whether previous validators returned an error or not.
You can implement attribute validators from the terraform-plugin-framework-validators Go module, which contains validation handling for many common use cases such as string contents and integer ranges.
If there is not an attribute validator in terraform-plugin-framework-validators that meets a specific use case, a provider-defined attribute validator can be created.
type stringLengthBetweenValidator struct{
Max int
Min int}// Description returns a plain text description of the validator's behavior, suitable for a practitioner to understand its impact.func(v stringLengthBetweenValidator)Description(ctx context.Context)string{return fmt.Sprintf("string length must be between %d and %d", v.Min, v.Max)}// MarkdownDescription returns a markdown formatted description of the validator's behavior, suitable for a practitioner to understand its impact.func(v stringLengthBetweenValidator)MarkdownDescription(ctx context.Context)string{return fmt.Sprintf("string length must be between `%d` and `%d`", v.Min, v.Max)}// Validate runs the main validation logic of the validator, reading configuration data out of `req` and updating `resp` with diagnostics.func(v stringLengthBetweenValidator)Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, resp *tfsdk.ValidateAttributeResponse){// types.String must be the attr.Value produced by the attr.Type in the schema for this attribute// for generic validators, use// https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/tfsdk#ConvertValue// to convert into a known type.var str types.String
diags := tfsdk.ValueAs(ctx, req.AttributeConfig,&str)
resp.Diagnostics.Append(diags...)if diags.HasError(){return}if str.Unknown || str.Null {return}
strLen :=len(str.Value)if strLen < v.Min || strLen > v.Max {
resp.Diagnostics.AddAttributeError(
req.AttributePath,"Invalid String Length",
fmt.Sprintf("String length must be between %d and %d, got: %d.", v.Min, v.Max, strLen),)return}}
type stringLengthBetweenValidator struct{ Max int Min int}// Description returns a plain text description of the validator's behavior, suitable for a practitioner to understand its impact.func(v stringLengthBetweenValidator)Description(ctx context.Context)string{return fmt.Sprintf("string length must be between %d and %d", v.Min, v.Max)}// MarkdownDescription returns a markdown formatted description of the validator's behavior, suitable for a practitioner to understand its impact.func(v stringLengthBetweenValidator)MarkdownDescription(ctx context.Context)string{return fmt.Sprintf("string length must be between `%d` and `%d`", v.Min, v.Max)}// Validate runs the main validation logic of the validator, reading configuration data out of `req` and updating `resp` with diagnostics.func(v stringLengthBetweenValidator)Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, resp *tfsdk.ValidateAttributeResponse){// types.String must be the attr.Value produced by the attr.Type in the schema for this attribute// for generic validators, use// https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/tfsdk#ConvertValue// to convert into a known type.var str types.String
diags := tfsdk.ValueAs(ctx, req.AttributeConfig,&str) resp.Diagnostics.Append(diags...)if diags.HasError(){return}if str.Unknown || str.Null {return} strLen :=len(str.Value)if strLen < v.Min || strLen > v.Max { resp.Diagnostics.AddAttributeError( req.AttributePath,"Invalid String Length", fmt.Sprintf("String length must be between %d and %d, got: %d.", v.Min, v.Max, strLen),)return}}
Optionally and depending on the complexity, it may be desirable to also create a helper function to instantiate the validator. For example:
Attribute validators that need to accept paths to reference other attribute data should instead prefer path expressions. This allows consumers to use either absolute paths starting at the root of a schema, or relative paths based on the current attribute path where the validator is called.
Path expressions may represent one or more actual paths in the data. To find those paths, the process is called path matching. Depending on the actual data, a path match may return a parent path for null or unknown values, since any underlying paths of those null or unknown values would also represent the same value. This framework behavior is used to prevent false positives of returning no paths for null or unknown values.
The general structure for working with path expressions in an attribute validator is:
If the attr.Value is not null and not unknown, then use tfsdk.ValueAs() using the expected value implementation as the target.
The following example shows a generic path based attribute validator that returns an error if types.Int64 values at the given path expressions are less than the current attribute types.Int64 value.
// Ensure our implementation satisfies the tfsdk.AttributeValidator interface.var_ tfsdk.AttributeValidator =&int64IsGreaterThanValidator{}// int64IsGreaterThanValidator is the underlying type implementing Int64IsGreaterThan.type int64IsGreaterThanValidator struct{
expressions path.Expressions
}// Description returns a plaintext string describing the validator.func(v int64IsGreaterThanValidator)Description(_ context.Context)string{return fmt.Sprintf("If configured, must be greater than %s attributes", v.expressions)}// MarkdownDescription returns a Markdown formatted string describing the validator.func(v int64IsGreaterThanValidator)MarkdownDescription(ctx context.Context)string{return v.Description(ctx)}// Validate performs the validation logic for the validator.func(v int64IsGreaterThanValidator)Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, resp *tfsdk.ValidateAttributeResponse){// If the current attribute configuration is null or unknown, there// cannot be any value comparisons, so exit early without error.if req.AttributeConfig.IsNull()|| req.AttributeConfig.IsUnknown(){return}// Convert the current attribute configuration to our expected attr.Value// implementation. This will raise an error if the validator is not put// on an attribute that is types.Int64Type.var attributeConfig types.Int64
diags := tftypes.ValueAs(ctx, req.AttributeConfig,&attributeConfig)
resp.Diagnostics.Append(diags...)// If the current attribute configuration is not valid, there cannot be// any value comparisons, so exit early.if resp.Diagnostics.HasError(){return}// Combine the given path expressions with the current attribute path// expression. This call automatically handles relative and absolute// expressions.
expressions := req.AttributePathExpression.MergeExpressions(v.expressions...)// For each expression, find matching paths.for_, expression :=range expressions {// Find paths matching the expression in the configuration data.
matchedPaths, diags := req.Config.PathMatches(ctx, expression)
resp.Diagnostics.Append(diags...)// Collect all errorsif diags.HasError(){continue}// For each matched path, get the data and compare.for_, matchedPath :=range matchedPaths {// Fetch the generic attr.Value at the given path. This ensures any// potential parent value of a different type, which can be a null// or unknown value, can be safely checked without raising a type// conversion error.var matchedPathValue attr.Value
diags := req.Config.GetAttribute(ctx, matchedPath,&matchedPathValue)
resp.Diagnostics.Append(diags...)// Collect all errorsif diags.HasError(){continue}// If the matched path value is null or unknown, we cannot compare// values, so continue to other matched paths.if matchedPathValue.IsNull()|| matchedPathValue.IsUnknown(){continue}// Now that we know the matched path value is not null or unknown,// it is safe to attempt converting it to the intended attr.Value// implementation, in this case a types.Int64 value.var matchedPathConfig types.Int64
diags = tftypes.ValueAs(ctx, matchedPathValue,&matchedPathConfig)
resp.Diagnostics.Append(diags...)// If the matched path value was not able to be converted from// attr.Value to the intended types.Int64 implementation, it most// likely means that the path expression was not pointing at a// types.Int64Type attribute. Collect the error and continue to// other matched paths.if diags.HasError(){continue}if matchedPathConfig.Value >= attributeConfig.Value {
resp.Diagnostics.AddAttributeError(
matchedPath,"Invalid Attribute Value",
fmt.Sprintf("Must be less than %s value: %d", req.AttributePath, attributeConfig.Value),)}}}}// Int64IsGreaterThan checks that any Int64 values in the paths described by the// path.Expression are less than the current attribute value.funcInt64IsGreaterThan(expressions ...path.Expression) tfsdk.AttributeValidator {return&int64IsGreaterThanValidator{
expressions: expressions,}}
// Ensure our implementation satisfies the tfsdk.AttributeValidator interface.var_ tfsdk.AttributeValidator =&int64IsGreaterThanValidator{}// int64IsGreaterThanValidator is the underlying type implementing Int64IsGreaterThan.type int64IsGreaterThanValidator struct{ expressions path.Expressions
}// Description returns a plaintext string describing the validator.func(v int64IsGreaterThanValidator)Description(_ context.Context)string{return fmt.Sprintf("If configured, must be greater than %s attributes", v.expressions)}// MarkdownDescription returns a Markdown formatted string describing the validator.func(v int64IsGreaterThanValidator)MarkdownDescription(ctx context.Context)string{return v.Description(ctx)}// Validate performs the validation logic for the validator.func(v int64IsGreaterThanValidator)Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, resp *tfsdk.ValidateAttributeResponse){// If the current attribute configuration is null or unknown, there// cannot be any value comparisons, so exit early without error.if req.AttributeConfig.IsNull()|| req.AttributeConfig.IsUnknown(){return}// Convert the current attribute configuration to our expected attr.Value// implementation. This will raise an error if the validator is not put// on an attribute that is types.Int64Type.var attributeConfig types.Int64
diags := tftypes.ValueAs(ctx, req.AttributeConfig,&attributeConfig) resp.Diagnostics.Append(diags...)// If the current attribute configuration is not valid, there cannot be// any value comparisons, so exit early.if resp.Diagnostics.HasError(){return}// Combine the given path expressions with the current attribute path// expression. This call automatically handles relative and absolute// expressions. expressions := req.AttributePathExpression.MergeExpressions(v.expressions...)// For each expression, find matching paths.for_, expression :=range expressions {// Find paths matching the expression in the configuration data. matchedPaths, diags := req.Config.PathMatches(ctx, expression) resp.Diagnostics.Append(diags...)// Collect all errorsif diags.HasError(){continue}// For each matched path, get the data and compare.for_, matchedPath :=range matchedPaths {// Fetch the generic attr.Value at the given path. This ensures any// potential parent value of a different type, which can be a null// or unknown value, can be safely checked without raising a type// conversion error.var matchedPathValue attr.Value
diags := req.Config.GetAttribute(ctx, matchedPath,&matchedPathValue) resp.Diagnostics.Append(diags...)// Collect all errorsif diags.HasError(){continue}// If the matched path value is null or unknown, we cannot compare// values, so continue to other matched paths.if matchedPathValue.IsNull()|| matchedPathValue.IsUnknown(){continue}// Now that we know the matched path value is not null or unknown,// it is safe to attempt converting it to the intended attr.Value// implementation, in this case a types.Int64 value.var matchedPathConfig types.Int64
diags = tftypes.ValueAs(ctx, matchedPathValue,&matchedPathConfig) resp.Diagnostics.Append(diags...)// If the matched path value was not able to be converted from// attr.Value to the intended types.Int64 implementation, it most// likely means that the path expression was not pointing at a// types.Int64Type attribute. Collect the error and continue to// other matched paths.if diags.HasError(){continue}if matchedPathConfig.Value >= attributeConfig.Value { resp.Diagnostics.AddAttributeError( matchedPath,"Invalid Attribute Value", fmt.Sprintf("Must be less than %s value: %d", req.AttributePath, attributeConfig.Value),)}}}}// Int64IsGreaterThan checks that any Int64 values in the paths described by the// path.Expression are less than the current attribute value.funcInt64IsGreaterThan(expressions ...path.Expression) tfsdk.AttributeValidator {return&int64IsGreaterThanValidator{ expressions: expressions,}}
You may want to create a custom type to simplify schemas if your provider contains common attribute values with consistent validation rules. When you implement validation on a type, you do not need to declare the same validation on the attribute, but you can supply additional validations in that manner. For example:
// Typically within the tfsdk.Schema returned by GetSchema() for a provider,// resource, or data source.
tfsdk.Attribute{// ... other Attribute configuration ...// This is an example type which implements its own validation
Type: computeInstanceIdentifierType,// This is optional, example validation that is checked in addition// to any validation performed by the type
Validators:[]AttributeValidators{
stringvalidator.LengthBetween(10,256),},}
// Typically within the tfsdk.Schema returned by GetSchema() for a provider,// resource, or data source.tfsdk.Attribute{// ... other Attribute configuration ...// This is an example type which implements its own validation Type: computeInstanceIdentifierType,// This is optional, example validation that is checked in addition// to any validation performed by the type Validators:[]AttributeValidators{ stringvalidator.LengthBetween(10,256),},}
Provider, resource, and data source schemas also support validation across all attributes. This is helpful when checking values in multiple attributes, such as ensuring the values are compatible with each other.
// Other methods to implement the provider.Provider interface are omitted for brevitytype exampleProvider struct{}func(p exampleProvider)ConfigValidators(ctx context.Context)[]provider.ConfigValidator {return[]provider.ConfigValidator{/* ... */}}
// Other methods to implement the provider.Provider interface are omitted for brevitytype exampleProvider struct{}func(p exampleProvider)ConfigValidators(ctx context.Context)[]provider.ConfigValidator {return[]provider.ConfigValidator{/* ... */}}
The provider.ProviderWithValidateConfig interface is more imperative in design and is useful for validating unique functionality that typically applies to a single provider. For example:
// Other methods to implement the provider.Provider interface are omitted for brevitytype exampleProvider struct{}func(p exampleProvider)ValidateConfig(ctx context.Context, req ValidateConfigRequest, resp *ValidateConfigResponse){// Retrieve values via req.Config.Get() or req.Config.GetAttribute(),// then return any warnings or errors via resp.Diagnostics.}
// Other methods to implement the provider.Provider interface are omitted for brevitytype exampleProvider struct{}func(p exampleProvider)ValidateConfig(ctx context.Context, req ValidateConfigRequest, resp *ValidateConfigResponse){// Retrieve values via req.Config.Get() or req.Config.GetAttribute(),// then return any warnings or errors via resp.Diagnostics.}
The framework performs resource schema validation in addition to any attribute and type validation. You can implement either or both of the following interfaces.
// Other methods to implement the resource.Resource interface are omitted for brevitytype exampleResource struct{}func(r exampleResource)ConfigValidators(ctx context.Context)[]resource.ConfigValidator {return[]resource.ConfigValidator{/* ... */}}
// Other methods to implement the resource.Resource interface are omitted for brevitytype exampleResource struct{}func(r exampleResource)ConfigValidators(ctx context.Context)[]resource.ConfigValidator {return[]resource.ConfigValidator{/* ... */}}
The resource.ResourceWithValidateConfig interface is more imperative in design and is useful for validating unique functionality that typically applies to a single resource. For example:
// Other methods to implement the resource.Resource interface are omitted for brevitytype exampleResource struct{}func(r exampleResource)ValidateConfig(ctx context.Context, req ValidateConfigRequest, resp *ValidateConfigResponse){// Retrieve values via req.Config.Get() or req.Config.GetAttribute(),// then return any warnings or errors via resp.Diagnostics.}
// Other methods to implement the resource.Resource interface are omitted for brevitytype exampleResource struct{}func(r exampleResource)ValidateConfig(ctx context.Context, req ValidateConfigRequest, resp *ValidateConfigResponse){// Retrieve values via req.Config.Get() or req.Config.GetAttribute(),// then return any warnings or errors via resp.Diagnostics.}
The framework performs data source schema validation in addition to any attribute and type validation. You can implement either or both of the following interfaces.
// Other methods to implement the datasource.DataSource interface are omitted for brevitytype exampleDataSource struct{}func(d exampleDataSource)ConfigValidators(ctx context.Context)[]datasource.ConfigValidator {return[]datasource.ConfigValidator{/* ... */}}
// Other methods to implement the datasource.DataSource interface are omitted for brevitytype exampleDataSource struct{}func(d exampleDataSource)ConfigValidators(ctx context.Context)[]datasource.ConfigValidator {return[]datasource.ConfigValidator{/* ... */}}
// Other methods to implement the datasource.DataSource interface are omitted for brevitytype exampleDataSource struct{}func(d exampleDataSource)ValidateConfig(ctx context.Context, req ValidateConfigRequest, resp *ValidateConfigResponse){// Retrieve values via req.Config.Get() or req.Config.GetAttribute(),// then return any warnings or errors via resp.Diagnostics.}
// Other methods to implement the datasource.DataSource interface are omitted for brevitytype exampleDataSource struct{}func(d exampleDataSource)ValidateConfig(ctx context.Context, req ValidateConfigRequest, resp *ValidateConfigResponse){// Retrieve values via req.Config.Get() or req.Config.GetAttribute(),// then return any warnings or errors via resp.Diagnostics.}