---
description: >-
  Learn how to use type constraints to validate user inputs to modules and
  resources.
---

# Type Constraints

OpenTofu module authors and provider developers can use detailed type
constraints to validate user-provided values for their input variables and
resource arguments. This requires some additional knowledge about OpenTofu's
type system, but allows you to build a more resilient user interface for your
modules and resources.

## Type Keywords and Constructors

Type constraints are expressed using a mixture of _type keywords_ and
function-like constructs called _type constructors._

* Type keywords are unquoted symbols that represent a static type.
* Type constructors are unquoted symbols followed by a pair of
  parentheses, which contain an argument that specifies more information about
  the type. Without its argument, a type constructor does not fully
  represent a type; instead, it represents a _kind_ of similar types.

Type constraints look like other kinds of OpenTofu
[expressions](index.mdx), but are a special syntax. Within the
OpenTofu language, they are only valid in the `type` argument of an
[input variable](../../language/values/variables.mdx).

## Primitive Types

A _primitive_ type is a simple type that isn't made from any other types. All
primitive types in OpenTofu are represented by a type keyword. The available
primitive types are:

* `string`: a sequence of Unicode characters representing some text, such
  as `"hello"`.
* `number`: a numeric value. The `number` type can represent both whole
  numbers like `15` and fractional values such as `6.283185`.
* `bool`: either `true` or `false`. `bool` values can be used in conditional
  logic.

### Conversion of Primitive Types

The OpenTofu language will automatically convert `number` and `bool` values
to `string` values when needed, and vice-versa as long as the string contains
a valid representation of a number or boolean value.

* `true` converts to `"true"`, and vice-versa
* `false` converts to `"false"`, and vice-versa
* `15` converts to `"15"`, and vice-versa

## Complex Types

A _complex_ type is a type that groups multiple values into a single value.
Complex types are represented by type constructors, but several of them
also have shorthand keyword versions.

There are two categories of complex types: collection types (for grouping
similar values), and structural types (for grouping potentially dissimilar
values).

### Collection Types

A _collection_ type allows multiple values of _one_ other type to be grouped
together as a single value. The type of value _within_ a collection is called
its _element type._ All collection types must have an element type, which is
provided as the argument to their constructor.

For example, the type `list(string)` means "list of strings", which is a
different type than `list(number)`, a list of numbers. All elements of a
collection must always be of the same type.

The three kinds of collection type in the OpenTofu language are:

* `list(...)`: a sequence of values identified by consecutive whole numbers
  starting with zero.

  The keyword `list` is a shorthand for `list(any)`, which accepts any
  element type as long as every element is the same type. This is for
  compatibility with older configurations; for new code, we recommend using
  the full form.
* `map(...)`: a collection of values where each is identified by a string label.

  The keyword `map` is a shorthand for `map(any)`, which accepts any
  element type as long as every element is the same type. This is for
  compatibility with older configurations; for new code, we recommend using
  the full form.

  Maps can be made with braces ({}) and colons (:) or equals signs (=):
  \{ "foo": "bar", "bar": "baz" } OR \{ foo = "bar", bar = "baz" }. Quotes
  may be omitted on keys, unless the key starts with a number, in which
  case quotes are required. Commas are required between key/value pairs
  for single line maps. A newline between key/value pairs is sufficient
  in multi-line maps.

  :::note
  Although colons are valid delimiters between keys and values, `tofu fmt` ignores them. In contrast, `tofu fmt` attempts to vertically align equals signs.
  :::
  
* `set(...)`: a collection of unique values that do not have any secondary
  identifiers or ordering.

### Structural Types

A _structural_ type allows multiple values of _several distinct types_ to be
grouped together as a single value. Structural types require a _schema_ as an
argument, to specify which types are allowed for which elements.

The two kinds of structural type in the OpenTofu language are:

* `object(...)`: a collection of named attributes that each have their own type.

  The schema for object types is `{ <KEY> = <TYPE>, <KEY> = <TYPE>, ... }` — a
  pair of curly braces containing a comma-separated series of `<KEY> = <TYPE>`
  pairs. Values that match the object type must contain _all_ of the specified
  keys, and the value for each key must match its specified type. (Values with
  _additional_ keys can still match an object type, but the extra attributes
  are discarded during type conversion.)
* `tuple(...)`: a sequence of elements identified by consecutive whole
  numbers starting with zero, where each element has its own type.

  The schema for tuple types is `[<TYPE>, <TYPE>, ...]` — a pair of square
  brackets containing a comma-separated series of types. Values that match the
  tuple type must have _exactly_ the same number of elements (no more and no
  fewer), and the value in each position must match the specified type for
  that position.

For example: an object type of `object({ name=string, age=number })` would match
a value like the following:

```hcl
{
  name = "John"
  age  = 52
}
```

Also, an object type of `object({ id=string, cidr_block=string })` would match
the object produced by a reference to an `aws_vpc` resource, like
`aws_vpc.example_vpc`; although the resource has additional attributes, they
would be discarded during type conversion.

Finally, a tuple type of `tuple([string, number, bool])` would match a value
like the following:

```hcl
["a", 15, true]
```

### Complex Type Literals

The OpenTofu language has literal expressions for creating tuple and object
values, which are described in
[Expressions: Literal Expressions](types.mdx#literal-expressions) as
"list/tuple" literals and "map/object" literals, respectively.

OpenTofu does _not_ provide any way to directly represent lists, maps, or sets.
However, due to the automatic conversion of complex types (described below), the
difference between similar complex types is almost never relevant to a normal
user, and most of the OpenTofu documentation conflates lists with tuples and
maps with objects. The distinctions are only useful when restricting input
values for a module or resource.

### Conversion of Complex Types

Similar kinds of complex types (list/tuple/set and map/object) can usually be
used interchangeably within the OpenTofu language, and most of OpenTofu's
documentation glosses over the differences between the kinds of complex type.
This is due to two conversion behaviors:

* Whenever possible, OpenTofu converts values between similar kinds of complex
  types if the provided value is not the exact type requested. "Similar kinds"
  is defined as follows:
  * Objects and maps are similar.
    * A map (or a larger object) can be converted to an object if it has
      _at least_ the keys required by the object schema. Any additional
      attributes are discarded during conversion, which means map -> object
      -> map conversions can be lossy.
  * Tuples and lists are similar.
    * A list can only be converted to a tuple if it has _exactly_ the
      required number of elements.
  * Sets are _almost_ similar to both tuples and lists:
    * When a list or tuple is converted to a set, duplicate values are
      discarded and the ordering of elements is lost.
    * When a `set` is converted to a list or tuple, the elements will be
      in an arbitrary order. If the set's elements were strings, they will
      be in lexicographical order; sets of other element types do not
      guarantee any particular order of elements.
* Whenever possible, OpenTofu converts _element values_ within a complex type,
  either by converting complex-typed elements recursively or as described above
  in [Conversion of Primitive Types](#conversion-of-primitive-types).

For example: if a module argument requires a value of type `list(string)` and a
user provides the tuple `["a", 15, true]`, OpenTofu will internally transform
the value to `["a", "15", "true"]` by converting the elements to the required
`string` element type. Later, if the module uses those elements to set different
resource arguments that require a string, a number, and a bool (respectively),
OpenTofu will automatically convert the second and third strings back to the
required types at that time, since they contain valid representations of a
number and a bool.

On the other hand, automatic conversion will fail if the provided value
(including any of its element values) is incompatible with the required type. If
an argument requires a type of `map(string)` and a user provides the object
`{name = ["Kristy", "Claudia", "Mary Anne", "Stacey"], age = 12}`, OpenTofu
will raise a type mismatch error, since a tuple cannot be converted to a string.

## Dynamic Types: The "any" Constraint

:::warning
`any` is very rarely the correct type constraint to use.
**Do not use `any` just to avoid specifying a type constraint**. Always write an
exact type constraint unless you are truly handling dynamic data.
:::

The keyword `any` is a special construct that serves as a placeholder for a
type yet to be decided. `any` is not _itself_ a type: when interpreting a
value against a type constraint containing `any`, OpenTofu will attempt to
find a single actual type that could replace the `any` keyword to produce
a valid result.

The only situation where it's appropriate to use `any` is if you will pass
the given value directly to some other system without directly accessing its
contents. For example, it's okay to use a variable of type `any` if you use
it only with `jsonencode` to pass the full value directly to a resource, as
shown in the following example:

```
variable "settings" {
  type = any
}

resource "aws_s3_object" "example" {
  # ...

  # This is a reasonable use of "any" because this module
  # just writes any given data to S3 as JSON, without
  # inspecting it further or applying any constraints
  # to its type or value.
  content = jsonencode(var.settings)
}
```

If any part of your module accesses elements or attributes of the value, or
expects it to be a string or number, or any other non-opaque treatment, it
is _incorrect_ to use `any`. Write the exact type that your module is expecting
instead.

### `any` with Collection Types

All of the elements of a collection must have the same type, so if you use
`any` as the placeholder for the element type of a collection then OpenTofu
will attempt to find a single exact element type to use for the resulting
collection.

For example, given the type constraint `list(any)`, OpenTofu will examine
the given value and try to choose a replacement for the `any` that would
make the result valid.

If the given value were `["a", "b", "c"]` -- whose physical type is
`tuple([string, string, string])` -- OpenTofu analyzes this as follows:

* Tuple types and list types are _similar_ per the previous section, so the
  tuple-to-list conversion rule applies.
* All of the elements in the tuple are strings, so the type constraint
  `string` would be valid for all of the list elements.
* Therefore in this case the `any` argument is replaced with `string`,
  and the final concrete value type is `list(string)`.

If the elements of the given tuple are not all of the same type then OpenTofu
will attempt to find a single type that they can all convert to. OpenTofu
will consider various conversion rules as described in earlier sections.

* If the given value were instead `["a", 1, "b"]` then OpenTofu would still
  select `list(string)`, because of the primitive type conversion rules, and
  the resulting value would be `["a", "1", "b"]` due to the string conversion
  implied by that type constraint.
* If the given value were instead `["a", [], "b"]` then the value cannot
  conform to the type constraint: there is no single type that both a string
  and an empty tuple can convert to. OpenTofu would reject this value,
  complaining that all elements must have the same type.

Although the above examples use `list(any)`, a similar principle applies to
`map(any)` and `set(any)`.

## Optional Object Type Attributes

OpenTofu typically returns an error when it does not receive a value for specified object attributes. When you mark an attribute as optional, OpenTofu instead inserts a default value for the missing attribute. This allows the receiving module to describe an appropriate fallback behavior.

To mark attributes as optional, use the `optional` modifier in the object type constraint. The following example creates optional attribute `b` and optional attribute with a default value `c`.

```hcl
variable "with_optional_attribute" {
  type = object({
    a = string                # a required attribute
    b = optional(string)      # an optional attribute
    c = optional(number, 127) # an optional attribute with default value
  })
}
```

The `optional` modifier takes one or two arguments.
- **Type:** (Required) The first argument
specifies the type of the attribute.
- **Default:** (Optional) The second argument defines the default value that OpenTofu should use if the attribute is not present. This must be compatible with the attribute type. If not specified, OpenTofu uses a `null` value of the appropriate type as the default.

An optional attribute with a non-`null` default value is guaranteed to never have the value `null` within the receiving module. OpenTofu will substitute the default value both when a caller omits the attribute altogether and when a caller explicitly sets it to `null`, thereby avoiding the need for additional checks to handle a possible null value.

OpenTofu applies object attribute defaults top-down in nested variable types. This means that OpenTofu applies the default value you specify in the `optional` modifier first and then later applies any nested default values to that attribute.

### Example: Nested Structures with Optional Attributes and Defaults

The following example defines a variable for storage buckets that host a website. This variable type uses several optional attributes, including `website`, which is itself an optional `object` type that has optional attributes and defaults.

```hcl
variable "buckets" {
  type = list(object({
    name    = string
    enabled = optional(bool, true)
    website = optional(object({
      index_document = optional(string, "index.html")
      error_document = optional(string, "error.html")
      routing_rules  = optional(string)
    }), {})
  }))
}
```

The following example `terraform.tfvars` file specifies three bucket configurations for `var.buckets`.

- `production` sets the routing rules to add a redirect
- `archived` uses default configuration, but is disabled
- `docs` overrides the index and error documents to use text files

The `production` bucket does not specify the index and error documents, and the `archived` bucket omits the website configuration entirely. OpenTofu will use the default values specified in the `bucket` type constraint.

```hcl
buckets = [
  {
    name = "production"
    website = {
      routing_rules = <<-EOT
      [
        {
          "Condition" = { "KeyPrefixEquals": "img/" },
          "Redirect"  = { "ReplaceKeyPrefixWith": "images/" }
        }
      ]
      EOT
    }
  },
  {
    name = "archived"
    enabled = false
  },
  {
    name = "docs"
    website = {
      index_document = "index.txt"
      error_document = "error.txt"
    }
  },
]
```

This configuration produces the following variable values.

- For the `production` and `docs` buckets, OpenTofu sets `enabled` to `true`. OpenTofu also supplies default values for `website`, and then the values specified in `docs` override those defaults.
- For the `archived` and `docs` buckets, OpenTofu sets `routing_rules` to a `null` value. When OpenTofu does not receive optional attributes and there are no specified defaults, OpenTofu populates those attributes with a `null` value.
- For the `archived` bucket, OpenTofu populates the `website` attribute with the default values specified in the `buckets` type constraint.

```hcl
tolist([
  {
    "enabled" = true
    "name" = "production"
    "website" = {
      "error_document" = "error.html"
      "index_document" = "index.html"
      "routing_rules" = <<-EOT
      [
        {
          "Condition" = { "KeyPrefixEquals": "img/" },
          "Redirect"  = { "ReplaceKeyPrefixWith": "images/" }
        }
      ]

      EOT
    }
  },
  {
    "enabled" = false
    "name" = "archived"
    "website" = {
      "error_document" = "error.html"
      "index_document" = "index.html"
      "routing_rules" = tostring(null)
    }
  },
  {
    "enabled" = true
    "name" = "docs"
    "website" = {
      "error_document" = "error.txt"
      "index_document" = "index.txt"
      "routing_rules" = tostring(null)
    }
  },
])
```

### Example: Conditionally setting an optional attribute

Sometimes the decision about whether or not to set a value for an optional argument needs to be made dynamically based on some other data. In that case, the calling `module` block can use a conditional expression with `null` as one of its result arms to represent dynamically leaving the argument unset.

With the `variable "buckets"` declaration shown in the previous section, the following example conditionally overrides the `index_document` and `error_document` settings in the `website` object based on a new variable `var.legacy_filenames`:

```hcl
variable "legacy_filenames" {
  type     = bool
  default  = false
  nullable = false
}

module "buckets" {
  source = "./modules/buckets"

  buckets = [
    {
      name = "maybe_legacy"
      website = {
        error_document = var.legacy_filenames ? "ERROR.HTM" : null
        index_document = var.legacy_filenames ? "INDEX.HTM" : null
      }
    },
  ]
}
```

When `var.legacy_filenames` is set to `true`, the call will override the document filenames. When it is `false`, the call will leave the two filenames unspecified, thereby allowing the module to use its specified default values.
