---
page_title: 'Plugin Development - Framework: Handling Data - Custom Types'
description: >-
  Custom Types.
---

# 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`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework-jsontypes)
    - JSON strings (both normalized and exact matching)
- [`terraform-plugin-framework-nettypes`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework-nettypes)
    - IPv4/IPv6 addresses and CIDRs
- [`terraform-plugin-framework-timetypes`](https://pkg.go.dev/github.com/hashicorp/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](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#StringAttribute.CustomType). 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.

```go
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:

```go
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](#semantic-equality) and/or [validation](#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.

</Tip>

| Framework Schema Type | Custom Schema Type Interface |
| --- | --- |
| [`basetypes.BoolType`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#BoolType) | [`basetypes.BoolTypable`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#BoolTypable) |
| [`basetypes.DynamicType`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#DynamicType) | [`basetypes.DynamicTypable`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#DynamicTypable) |
| [`basetypes.Float32Type`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#Float32Type) | [`basetypes.Float32Typable`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#Float32Typable) |
| [`basetypes.Float64Type`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#Float64Type) | [`basetypes.Float64Typable`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#Float64Typable) |
| [`basetypes.Int32Type`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#Int32Type) | [`basetypes.Int32Typable`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#Int32Typable) |
| [`basetypes.Int64Type`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#Int64Type) | [`basetypes.Int64Typable`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#Int64Typable) |
| [`basetypes.ListType`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#ListType) | [`basetypes.ListTypable`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#ListTypable) |
| [`basetypes.MapType`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#MapType) | [`basetypes.MapTypable`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#MapTypable) |
| [`basetypes.NumberType`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#NumberType) | [`basetypes.NumberTypable`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#NumberTypable) |
| [`basetypes.ObjectType`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#ObjectType) | [`basetypes.ObjectTypable`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#ObjectTypable) |
| [`basetypes.SetType`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#SetType) | [`basetypes.SetTypable`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#SetTypable) |
| [`basetypes.StringType`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#StringType) | [`basetypes.StringTypable`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#StringTypable) |

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`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/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 example `basetypes.StringTypable` is defined as [`ValueFromString`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#StringTypable)

In this example, the `basetypes.StringTypable` interface is implemented to create a custom string type with an associated [value type](#value-type):

```go
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.

</Tip>

| Framework Schema Type | Custom Schema Type Interface |
| --- | --- |
| [`basetypes.BoolValue`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#BoolValue) | [`basetypes.BoolValuable`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#BoolValuable) |
| [`basetypes.DynamicValue`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#DynamicValue) | [`basetypes.DynamicValuable`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#DynamicValuable) |
| [`basetypes.Float64Value`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#Float64Value) | [`basetypes.Float64Valuable`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#Float64Valuable) |
| [`basetypes.Int64Value`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#Int64Value) | [`basetypes.Int64Valuable`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#Int64Valuable) |
| [`basetypes.ListValue`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#ListValue) | [`basetypes.ListValuable`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#ListValuable) |
| [`basetypes.MapValue`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#MapValue) | [`basetypes.MapValuable`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#MapValuable) |
| [`basetypes.NumberValue`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#NumberValue) | [`basetypes.NumberValuable`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#NumberValuable) |
| [`basetypes.ObjectValue`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#ObjectValue) | [`basetypes.ObjectValuable`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#ObjectValuable) |
| [`basetypes.SetValue`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#SetValue) | [`basetypes.SetValuable`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#SetValuable) |
| [`basetypes.StringValue`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#StringValue) | [`basetypes.StringValuable`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#StringValuable) |

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`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/attr#Value) methods must be overridden by the custom type to prevent confusing errors:

- `Equal(attr.Value) bool`
- `Type(context.Context) attr.Type`

<Note>

The overridden `Equal(attr.Value) bool` method should not contain [Semantic Equality](#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.

</Note>

In this example, the `basetypes.StringValuable` interface is implemented to create a custom string value type with an associated [schema type](#schema-type):

```go
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` or `Update` 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:

```go
// 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](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/attr/xattr#ValidateableAttribute) 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](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#ValidateableParameter) 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.

```go
// 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](#value-validation) for more information.

The [`xattr.TypeWithValidate` interface](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/attr/xattr#TypeWithValidate) has been deprecated. Use the [`xattr.ValidateableAttribute` interface](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/attr/xattr#ValidateableAttribute), and [`function.ValidateableParameter` interface](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#ValidateableParameter) instead.

</Note>

Implement the [`xattr.TypeWithValidate`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/attr/xattr#TypeWithValidate) interface on the value type to define and enable this behavior.

<Note>

This functionality uses the lower level [`tftypes`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-go/tftypes) type system compared to other framework logic.

</Note>

In this example, the custom string value type will ensure the string is a valid RFC3339 timestamp:

```go
// 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
}
```
