A list of objects with dynamic type elements

Viewed 535

I have a list of objects in which one element operand could be of dynamic type a string, number or a map. I defined the variable as follow:

variable "settings" {
  type = list(object({
    type=string
    operator=string
    operand=any
    ...
  }))
}

And from calling module, assigning values like below:

 settings = [    
    { "operator" = "lessThan", "type" = "responseTime", "operand" = 10000 },
    { "operator" = "is", "type" = "statusCode", "operand" = 200 },
    { "operator" = "is", "property" = "content-type", "type" = "header", "operand" = "abc" },
    { "operator" = "validatesJSONPath", "type" = "body", "operand" = { "operator" = "contains", "operandValue" = "google", "jsonPath" = "id" } }
  ]

However, this is throwing me following error:

The given value is not suitable for child module variable "settings" defined at operator\variables.tf:64,1-21: cannot find a common base type for all elements.

How best can I address this?

1 Answers

When thinking about Terraform type constraints, you should understand any as asking Terraform to automatically infer a concrete type to replace it. any isn't a type in itself, but is instead a placeholder for one.

In your case, you've effectively asked Terraform to infer an element type for the list which matches the constraint. This plays out a little differently than the simpler case of list(any), but still has the same constraint: Terraform needs to find one single concrete type to replace any with, and then convert all of the list elements to be of that type.


The situation you're describing seems like you'd prefer Terraform to behave like a dynamically-typed language rather than a statically-typed one, where it just takes whatever value you give it and then check dynamically whether it's shaped in a suitable way, as in duck typing.

You can achieve that in Terraform by setting type = any, which asks Terraform to automatically infer a type for the entire value, which effectively applies no constraint at all because the value already has a concrete type and so any will be replaced with that type directly.

If you wish you can then just use the value elsewhere in your module, like var.settings[0].operand.operator, and Terraform will succeed in evaluating that if the structure matches and raise a dynamic operation error if not, just as we typically see in dynamic programming languages like Python.

You can also find a middle-ground by imposing some validation checks on the variable itself, which is roughly equivalent to starting a Python function with some explicit validation of each of the arguments and returning an error if they don't "quack like a duck":

variable "settings" {
  type = any

  validation {
    condition     = alltrue([for o in var.settings : can(tostring(o.operator))])
    error_message = "Each element must be an object with a string attribute named \"operator\"."
  }

  # ... and any other similar validation rules you want
}

A nice benefit of using explicit validation blocks like this, rather than just letting expressions fail inline inside the module, is that Terraform will attribute these validation failures to the argument inside the calling module block, rather than to some implementation detail inside the module. It will therefore appear to the caller as a problem with their module call, rather than a bug in the module itself.

Related