Validation: an attribute at the given path has a specific value

Greetings everyone!

I’m using the following libraries to develop a plugin:

github.com/hashicorp/terraform-plugin-framework v1.1.1
github.com/hashicorp/terraform-plugin-framework-validators v0.10.0

Is there a way to set up validation to verify that an attribute at the given pass has a specific value?

Example

The validation should pass when graph.code block exists and the graph.mode equals code.

  1. Should pass (code block exists, mode = code):
data "gdashboard_text" "test" {
  title = "Test"

  graph {
    mode = "code"
    code {
      language = "sql"
    }
  }
}
  1. Should pass (code block does not exist, so the value of the mode does not matter):
data "gdashboard_text" "test" {
  title = "Test"

  graph {
    mode = "markdown"
  }
}
  1. Should fail (code block exists, mode != code):
data "gdashboard_text" "test" {
  title = "Test"

  graph {
    mode = "markdown"
    code {
      language = "sql"
    }
  }
}

Implementation attempts

The entire codebase is available on GitHub.

Here is my definition of the graph block:

func graphBlock() schema.Block {
	return schema.ListNestedBlock{
		NestedObject: schema.NestedBlockObject{
			Blocks: map[string]schema.Block{
				"code": schema.ListNestedBlock{
					NestedObject: schema.NestedBlockObject{
						Attributes: map[string]schema.Attribute{
							"language": schema.StringAttribute{
								Optional: true,
							},
						},
						Validators: []validator.Object{
							objectvalidator.ConflictsWith(
								path.MatchRoot("graph").AtAnyListIndex().AtName("mode").AtSetValue(types.StringValue("markdown")),
							),
						},
					},
				},
			},
			Attributes: map[string]schema.Attribute{
				"mode": schema.StringAttribute{
					Optional:            true,
					Validators: []validator.String{
						stringvalidator.OneOf("markdown", "code"),
					},
				},
			},
		},
	}
}

The validation is defined as:

objectvalidator.ConflictsWith(
path.MatchRoot("graph").AtAnyListIndex().AtName("mode").AtSetValue(types.StringValue("markdown"))
)

If I get it right, the validation will fail if the graph[*].mode == markdown. Since the validation is defined in the code nested object, it’s triggered only when the code block is defined.

Unfortunately, it fails with:

The Terraform Provider unexpectedly provided a path expression that does not
        match the current schema. This can happen if the path expression does not
        correctly follow the schema in structure or types. Please report this to the
        provider developers.
        
        Path Expression: graph[*].mode[Value("markdown")]

The path (before AtSetValue) is correct, because switching to:

path.MatchRoot("graph").AtAnyListIndex().AtName("mode")

gives me a reasonable error:

Attribute "graph[0].mode" cannot be specified when "graph[0].code[0]" is specified

So, is there a way to validate that the attribute at the given path has a specific value?

Hi @iRevive :wave:

One way to achieve this is to use a custom validator.

Assuming the following schema:

schema.Schema{
		Attributes: map[string]schema.Attribute{
			"title": schema.StringAttribute{
				Required: true,
			},
		},

		// Nested Blocks
		Blocks: map[string]schema.Block{
			"graph": schema.ListNestedBlock{
				NestedObject: schema.NestedBlockObject{
					Blocks: map[string]schema.Block{
						"code": schema.ListNestedBlock{
							NestedObject: schema.NestedBlockObject{
								Attributes: map[string]schema.Attribute{
									"language": schema.StringAttribute{
										Optional: true,
									},
								},
							},
							Validators: []validator.List{
								CheckMode("code"),
							},
						},
					},
					Attributes: map[string]schema.Attribute{
						"mode": schema.StringAttribute{
							Optional: true,
						},
					},
				},
			},
		},
	}

The custom CheckMode validator could then look something like the following:

var _ validator.List = checkModeValidator{}

type checkModeValidator struct {
	mode string
}

func (v checkModeValidator) Description(_ context.Context) string {
	return fmt.Sprintf("mode must be set to %s", v.mode)
}

func (v checkModeValidator) MarkdownDescription(ctx context.Context) string {
	return v.Description(ctx)
}

func (v checkModeValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) {
	if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() {
		// If code block does not exist, config is valid.
		return
	}

	modePath := req.Path.ParentPath().AtName("mode")

	var m types.String

	diags := req.Config.GetAttribute(ctx, modePath, &m)
	resp.Diagnostics.Append(diags...)

	if resp.Diagnostics.HasError() {
		return
	}

	if m.IsNull() || m.IsUnknown() {
		// Only validate if mode value is known.
		return
	}

	if m.ValueString() != v.mode {
		resp.Diagnostics.AddAttributeError(
			modePath,
			"Mode value invalid",
			v.Description(ctx),
		)
	}
}

func CheckMode(m string) validator.List {
	return checkModeValidator{
		mode: m,
	}
}

There is some documentation concerning the usage of Custom Validators within the guide for migrating a provider from SDKv2 to the Framework.

1 Like

Thanks for the detailed response! I will experiment with Custom Validator. It fits perfectly from the very first view.

Hi there @iRevive and @bendbennett,

Thanks to both of you: For asking the question, and for providing the code snippet.

It turns out I had a scenario where this pattern fit perfectly, and now I’m using it.

1 Like