Terraform
Custom Types
Use existing custom types or develop custom types to consistently define behaviors for a kind of value across schemas. Custom types are supported on top of any framework-defined type.
Supported behaviors for custom types include:
- Semantic Equality: Keep a prior value if a new value is inconsequentially different, such as preventing drift detection of JSON strings with differing property ordering.
- Validation: Raise warning and/or error diagnostics based on the value, such as not following a specified format or enumeration of values.
Example Use Cases
- Encoded values, such as an XML or YAML string.
- Provider-specific values, such as an AWS ARN or AzureRM location string.
- Networking values, such as a MAC address.
- Time values, such as an ISO 8601 string.
Common Custom Types
The following Go modules contain custom type implementations covering common use cases with validation and semantic equality logic (where appropriate).
terraform-plugin-framework-jsontypes
- JSON strings (both normalized and exact matching)
terraform-plugin-framework-nettypes
- IPv4/IPv6 addresses and CIDRs
terraform-plugin-framework-timetypes
- Timestamps (such as RFC3339)
Concepts
Individual data value handling in the framework is performed by a pair of associated Go types:
- Schema Types: Define the associated value type and logic to create a value.
- Value Types: Define behaviors associated with the value, data storage of the value, and data storage of the value state (null, unknown, or known).
The framework defines a standard set these associated Go types referred to by the "base type" terminology. Extending these base types is referred to by the "custom type" terminology.
Using Custom Types
Use a custom type by switching the schema definition and data handling from a framework-defined type to the custom type.
Schema Definition
The framework schema types accept a CustomType
field where applicable, such as the resource/schema.StringAttribute
type. When the CustomType
is omitted, the framework defaults to the associated base type.
Implement the CustomType
field in a schema type to switch from the base type to a custom type.
In this example, a string attribute implements a custom type.
schema.StringAttribute{
CustomType: CustomStringType{},
// ... other fields ...
}
Data Handling
Each custom type will also include a value type, which must be used anywhere the value is referenced in data source, provider, or resource logic.
Switch any usage of a base value type to the custom value type. Any logic will need to be updated to match the custom value type implementation.
In this example, a custom value type is used in a data model approach:
type ThingModel struct {
// Instead of types.String
Timestamp CustomStringValue `tfsdk:"timestamp"`
// ... other fields ...
}
Developing Custom Types
Create a custom type by extending an existing framework schema type and its associated value type. Once created, define semantic equality and/or validation logic for the custom type.
Schema Type
Extend a framework schema type by creating a Go type that implements one of the github.com/hashicorp/terraform-plugin-framework/types/basetypes
package *Typable
interfaces.
Tip
The commonly used types
package types are aliases to the basetypes
package types mentioned in this table.
It is recommended to use Go type embedding of the base type to simplify the implementation and ensure it is up to date with the latest data handling features of the framework. With type embedding, the following attr.Type
methods must be overridden by the custom type to prevent confusing errors:
Equal(attr.Type) bool
ValueFromTerraform(context.Context, tftypes.Value) (attr.Value, error)
ValueType(context.Context) attr.Value
String() string
ValueFrom{TYPE}(context.Context, basetypes.{TYPE}Value) (basetypes.{TYPE}Valuable, diag.Diagnostics)
- This method signature is different for each
*Typable
custom schema type interface listed above, for examplebasetypes.StringTypable
is defined asValueFromString
- This method signature is different for each
In this example, the basetypes.StringTypable
interface is implemented to create a custom string type with an associated value type:
import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/hashicorp/terraform-plugin-go/tftypes"
)
// Ensure the implementation satisfies the expected interfaces
var _ basetypes.StringTypable = CustomStringType{}
type CustomStringType struct {
basetypes.StringType
// ... potentially other fields ...
}
func (t CustomStringType) Equal(o attr.Type) bool {
other, ok := o.(CustomStringType)
if !ok {
return false
}
return t.StringType.Equal(other.StringType)
}
func (t CustomStringType) String() string {
return "CustomStringType"
}
func (t CustomStringType) ValueFromString(ctx context.Context, in basetypes.StringValue) (basetypes.StringValuable, diag.Diagnostics) {
// CustomStringValue defined in the value type section
value := CustomStringValue{
StringValue: in,
}
return value, nil
}
func (t CustomStringType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) {
attrValue, err := t.StringType.ValueFromTerraform(ctx, in)
if err != nil {
return nil, err
}
stringValue, ok := attrValue.(basetypes.StringValue)
if !ok {
return nil, fmt.Errorf("unexpected value type of %T", attrValue)
}
stringValuable, diags := t.ValueFromString(ctx, stringValue)
if diags.HasError() {
return nil, fmt.Errorf("unexpected error converting StringValue to StringValuable: %v", diags)
}
return stringValuable, nil
}
func (t CustomStringType) ValueType(ctx context.Context) attr.Value {
// CustomStringValue defined in the value type section
return CustomStringValue{}
}
Value Type
Extend a framework value type by creating a Go type that implements one of the github.com/hashicorp/terraform-plugin-framework/types/basetypes
package *Valuable
interfaces.
Tip
The commonly used types
package types are aliases to the basetypes
package types mentioned in this table.
It is recommended to use Go type embedding of the base type to simplify the implementation and ensure it is up to date with the latest data handling features of the framework. With type embedding, the following attr.Value
methods must be overridden by the custom type to prevent confusing errors:
Note
The overridden Equal(attr.Value) bool
method should not contain Semantic Equality logic. Equal
should only check the type of attr.Value
and the underlying base value.
An example of this can be found below with the CustomStringValue
implementation.
In this example, the basetypes.StringValuable
interface is implemented to create a custom string value type with an associated schema type:
import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/hashicorp/terraform-plugin-go/tftypes"
)
// Ensure the implementation satisfies the expected interfaces
var _ basetypes.StringValuable = CustomStringValue{}
type CustomStringValue struct {
basetypes.StringValue
// ... potentially other fields ...
}
func (v CustomStringValue) Equal(o attr.Value) bool {
other, ok := o.(CustomStringValue)
if !ok {
return false
}
return v.StringValue.Equal(other.StringValue)
}
func (v CustomStringValue) Type(ctx context.Context) attr.Type {
// CustomStringType defined in the schema type section
return CustomStringType{}
}
From this point, the custom type can be extended with other behaviors.
Semantic Equality
Semantic equality handling enables the value type to automatically keep a prior value when a new value is determined to be inconsequentially different. This handling can prevent unexpected drift detection for values and in some cases prevent Terraform data handling errors.
This value type functionality is checked in the following scenarios:
- When refreshing a data source, the response state value from the
Read
method logic is compared to the configuration value. - When refreshing a resource, the response new state value from the
Read
method logic is compared to the request prior state value. - When creating or updating a resource, the response new state value from the
Create
orUpdate
method logic is compared to the request plan value.
The framework will only call semantic equality logic if both the prior and new values are known. Null or unknown values are unnecessary to check. When working with collection types, the framework automatically calls semantic equality logic of element types. When working with object types, the framework automatically calls semantic equality of underlying attribute types.
Implement the associated github.com/hashicorp/terraform-plugin-framework/types/basetypes
package *ValuableWithSemanticEquals
interface on the value type to define and enable this behavior.
In this example, the custom string value type will preserve the prior value if the expected RFC3339 timestamps are considered equivalent:
// CustomStringValue defined in the value type section
// Ensure the implementation satisfies the expected interfaces
var _ basetypes.StringValuableWithSemanticEquals = CustomStringValue{}
func (v CustomStringValue) StringSemanticEquals(ctx context.Context, newValuable basetypes.StringValuable) (bool, diag.Diagnostics) {
var diags diag.Diagnostics
// The framework should always pass the correct value type, but always check
newValue, ok := newValuable.(CustomStringValue)
if !ok {
diags.AddError(
"Semantic Equality Check Error",
"An unexpected value type was received while performing semantic equality checks. "+
"Please report this to the provider developers.\n\n"+
"Expected Value Type: "+fmt.Sprintf("%T", v)+"\n"+
"Got Value Type: "+fmt.Sprintf("%T", newValuable),
)
return false, diags
}
// Skipping error checking if CustomStringValue already implemented RFC3339 validation
priorTime, _ := time.Parse(time.RFC3339, v.StringValue.ValueString())
// Skipping error checking if CustomStringValue already implemented RFC3339 validation
newTime, _ := time.Parse(time.RFC3339, newValue.ValueString())
// If the times are equivalent, keep the prior value
return priorTime.Equal(newTime), diags
}
Validation
Value Validation
Validation handling in custom value types can be enabled for schema attribute values, or provider-defined function parameters.
Implement the xattr.ValidateableAttribute
interface on the custom value type to define and enable validation handling for a schema attribute, which will automatically raise warning and/or error diagnostics when a value is determined to be invalid.
Implement the function.ValidateableParameter
interface on the custom value type to define and enable validation handling for a provider-defined function parameter, which will automatically raise an error when a value is determined to be invalid.
If the custom value type is to be used for both schema attribute values and provider-defined function parameters, implement both interfaces.
// Implementation of the xattr.ValidateableAttribute interface
func (v CustomStringValue) ValidateAttribute(ctx context.Context, req xattr.ValidateAttributeRequest, resp *xattr.ValidateAttributeResponse) {
if v.IsNull() || v.IsUnknown() {
return
}
err := v.validate(v.ValueString())
if err != nil {
resp.Diagnostics.AddAttributeError(
req.Path,
"Invalid RFC 3339 String Value",
"An unexpected error occurred while converting a string value that was expected to be RFC 3339 format. "+
"The RFC 3339 string format is YYYY-MM-DDTHH:MM:SSZ, such as 2006-01-02T15:04:05Z or 2006-01-02T15:04:05+07:00.\n\n"+
"Path: "+req.Path.String()+"\n"+
"Given Value: "+v.ValueString()+"\n"+
"Error: "+err.Error(),
)
return
}
}
// Implementation of the function.ValidateableParameter interface
func (v CustomStringValue) ValidateParameter(ctx context.Context, req function.ValidateParameterRequest, resp *function.ValidateParameterResponse) {
if v.IsNull() || v.IsUnknown() {
return
}
err := v.validate(v.ValueString())
if err != nil {
resp.Error = function.NewArgumentFuncError(
req.Position,
"Invalid RFC 3339 String Value: "+
"An unexpected error occurred while converting a string value that was expected to be RFC 3339 format. "+
"The RFC 3339 string format is YYYY-MM-DDTHH:MM:SSZ, such as 2006-01-02T15:04:05Z or 2006-01-02T15:04:05+07:00.\n\n"+
fmt.Sprintf("Position: %d", req.Position)+"\n"+
"Given Value: "+v.ValueString()+"\n"+
"Error: "+err.Error(),
)
}
}
func (v CustomStringValue) validate(in string) error {
_, err := time.Parse(time.RFC3339, in)
return err
}
Type Validation
Note
Value
validation should be used in preference to Type
validation. Refer to Value Validation for more information.
The xattr.TypeWithValidate
interface has been deprecated. Use the xattr.ValidateableAttribute
interface, and function.ValidateableParameter
interface instead.
Implement the xattr.TypeWithValidate
interface on the value type to define and enable this behavior.
Note
This functionality uses the lower level tftypes
type system compared to other framework logic.
In this example, the custom string value type will ensure the string is a valid RFC3339 timestamp:
// CustomStringType defined in the schema type section
func (t CustomStringType) Validate(ctx context.Context, value tftypes.Value, valuePath path.Path) diag.Diagnostics {
if value.IsNull() || !value.IsKnown() {
return nil
}
var diags diag.Diagnostics
var valueString string
if err := value.As(&valueString); err != nil {
diags.AddAttributeError(
valuePath,
"Invalid Terraform Value",
"An unexpected error occurred while attempting to convert a Terraform value to a string. "+
"This generally is an issue with the provider schema implementation. "+
"Please contact the provider developers.\n\n"+
"Path: "+valuePath.String()+"\n"+
"Error: "+err.Error(),
)
return diags
}
if _, err := time.Parse(time.RFC3339, valueString); err != nil {
diags.AddAttributeError(
valuePath,
"Invalid RFC 3339 String Value",
"An unexpected error occurred while converting a string value that was expected to be RFC 3339 format. "+
"The RFC 3339 string format is YYYY-MM-DDTHH:MM:SSZ, such as 2006-01-02T15:04:05Z or 2006-01-02T15:04:05+07:00.\n\n"+
"Path: "+valuePath.String()+"\n"+
"Given Value: "+valueString+"\n"+
"Error: "+err.Error(),
)
return diags
}
return diags
}