Skip to main content

Attributes as Blocks

note

This page is an appendix to the OpenTF documentation. Most users do not need to know the full details of this behavior.

Summary

Many resource types use repeatable nested blocks to manage collections of sub-objects related to the primary resource.

Rarely, some resource types also support an argument with the same name as a nested block type, and will purge any sub-objects of that type if that argument is set to an empty list (<ATTR> = []).

Most users do not need to know any further details of this "nested block or empty list" behavior. However, read further if you need to:

  • Use OpenTF's JSON syntax with this type of resource.
  • Create a reusable module that wraps this type of resource.

Details

The language makes a distinction between argument syntax and nested block syntax within blocks:

  • Argument syntax sets a named argument for the containing object. If the attribute has a default value then an explicitly-specified value entirely overrides that default.

  • Nested block syntax represents a related child object of the container that has its own set of arguments. Where multiple such objects are possible, multiple blocks of the same type can be present. If the nested attributes themselves have default values, they are honored for each nested block separately, merging in with any explicitly-defined arguments.

The distinction between these is particularly important for JSON syntax because the same primitive JSON constructs (lists and objects) will be interpreted differently depending on whether a particular name is an argument or a nested block type.

Defining a Fixed Object Collection Value

When working with resource type arguments that behave in this way, it is valid and we recommend to use the nested block syntax whenever defining a fixed collection of objects:

example {
foo = "bar"
}
example {
foo = "baz"
}

The above implicitly specifies a two-element list of objects assigned to the example argument, treating it as if it were a nested block type.

If you need to explicitly call for zero example objects, you must use the argument syntax with an empty list:

example = []

These two forms cannot be mixed; there cannot be both explicitly zero example objects and explicit single example blocks declared at the same time.

For true nested blocks where this special behavior does not apply, assigning [] using argument syntax is not valid. The normal way to specify zero objects of a type is to write no nested blocks at all.

Arbitrary Expressions with Argument Syntax

Although we recommend using block syntax for simple cases for readability, the names that work in this mode are defined as arguments, and so it is possible to use argument syntax to assign arbitrary dynamic expressions to them, as long as the expression has the expected result type:

example = [
for name in var.names: {
foo = name
}
]
# Not recommended, but valid: a constant list-of-objects expression
example = [
{
foo = "bar"
},
{
foo = "baz"
},
]

Because of the rule that argument declarations like this fully override any default value, when creating a list-of-objects expression directly the usual handling of optional arguments does not apply, so all of the arguments must be assigned a value, even if it's an explicit null:

example = [
{
# Cannot omit foo in this case, even though it would be optional in the
# nested block syntax.
foo = null
},
]

If you are writing a reusable module that allows callers to pass in a list of objects to assign to such an argument, you may wish to use the merge function to populate any attributes the user didn't explicitly set, in order to give the module user the effect of optional arguments:

example = [
for ex in var.examples: merge({
foo = null # (or any other suitable default value)
}, ex)
]

For the arguments that use the attributes-as-blocks usage mode, the above is a better pattern than using dynamic blocks because the case where the caller provides an empty list will result in explicitly assigning an empty list value, rather than assigning no value at all and thus retaining and ignoring any existing objects. dynamic blocks are required for dynamically-generating normal nested blocks, though.

In JSON syntax

Arguments that use this special mode are specified in JSON syntax always using the JSON expression mapping to produce a list of objects.

The interpretation of these values in JSON syntax is, therefore, equivalent to that described under Arbitrary Expressions with Argument Syntax above, but expressed in JSON syntax instead.

Due to the ambiguity of the JSON syntax, there is no way to distinguish based on the input alone between argument and nested block usage, so the JSON syntax cannot support the nested block processing mode for these arguments. This is, unfortunately, one necessary concession on the equivalence between native syntax and JSON syntax made pragmatically for compatibility with existing provider design patterns. Providers may phase out such patterns in future major releases.