Need help with your JSON?

Try our JSON Formatter tool to automatically identify and fix syntax errors in your JSON. JSON Formatter tool

Terraform JSON Configuration File Best Practices

If you searched for Terraform JSON provider syntax, the key point is that .tf.json is not a separate Terraform feature. It is the JSON representation of normal Terraform configuration, and it works best when another program is generating the files for you. For infrastructure people edit directly, plain.tf remains the better default.

The biggest mistakes teams make are putting provider requirements in the wrong place, confusing literal strings with Terraform expressions, and assuming terraform fmt can convert HCL into JSON. This guide focuses on the current behavior that matters most for real-world .tf.json modules.

When .tf.json Is The Right Tool

Use JSON syntax when your source of truth already lives in code, templates, a catalog, or another system that can emit valid JSON. Terraform loads .tf and .tf.json files together, so you do not need to choose one format for an entire module.

  • Use .tf.json for generated configuration: It is a good target for code generators, scaffolding tools, or internal platforms that already work with JSON objects.
  • Keep hand-authored modules in HCL: HCL is easier to review, comment, refactor, and debug when humans are changing the configuration often.
  • Do not switch formats just because one argument needs JSON: If a provider attribute expects JSON text, keep the Terraform file in HCL and use jsonencode() for that specific value.

Provider Syntax In .tf.json

For searchers looking for Terraform JSON configuration syntax around providers, the important split is: terraform.required_providers declares where providers come from, top-level provider blocks configure them, and resource or module arguments choose which configured instance to use.

Provider-Focused .tf.json Example

{
  "//": "Generated by tools/generate-terraform-json.ts",
  "terraform": {
    "required_version": ">= 1.0.0",
    "required_providers": {
      "aws": {
        "source": "hashicorp/aws",
        "version": "~> 5.0"
      }
    }
  },
  "provider": {
    "aws": [
      {
        "region": "us-east-1"
      },
      {
        "alias": "west",
        "region": "us-west-2"
      }
    ]
  },
  "resource": {
    "aws_s3_bucket": {
      "logs": {
        "provider": "aws.west",
        "bucket": "example-logs-bucket"
      }
    }
  },
  "module": {
    "network": {
      "source": "./modules/network",
      "providers": {
        "aws": "aws.west"
      }
    }
  }
}
  1. Declare provider requirements under the top-level terraform block: required_providers is where you set the provider local name, source address, and version constraint.
  2. Configure providers separately: The top-level provider object contains one or more provider configuration blocks. When you need multiple configurations, such as an aliased provider, the JSON form is an array of objects under that provider name.
  3. Use literal provider addresses when selecting an alias: Resource-level provider values and module-level providers mappings use strings like aws.west, not interpolation syntax.
  4. Keep provider versions out of provider configuration: Version constraints belong in required_providers, not in the provider configuration block itself.

If you still maintain Terraform 0.12.26, it accepts the object form of required_providers but ignores the source setting. Current Terraform versions should use the modern object form shown above.

Expression Rules That Trip People Up

JSON does not have Terraform's native expression syntax, so Terraform maps JSON values into expression contexts. That mapping is the main reason generated files are easy to get subtly wrong.

  • In expression positions, JSON strings are parsed as Terraform string templates: "web-${var.environment}" and "${var.instance_count}" are both valid patterns inside arguments that accept Terraform expressions.
  • If the whole string is a single interpolation, Terraform can use the underlying value type: that matters for numbers, booleans, lists, maps, and references passed through generated JSON.
  • Some arguments are literal-only: Common examples are terraform.required_version, required_providers.source, required_providers.version, module.source, and provider aliases.
  • Comments are limited but not impossible: A property named "//" is ignored at the root level and inside objects that represent block bodies, which makes it useful for generator notes.

Expression Example

{
  "resource": {
    "aws_instance": {
      "web": {
        "count": "${var.instance_count}",
        "ami": "ami-0abcdef1234567890",
        "instance_type": "t3.micro",
        "tags": {
          "Name": "web-${var.environment}"
        }
      }
    }
  }
}

Best Practices For Maintainable .tf.json

  1. Use JSON syntax as a generation target, not a hand-editing format: If engineers need to review and edit the configuration daily, keep the source in HCL and generate only the parts that truly need JSON.
  2. Make provider requirements explicit from the start: Declare every provider in terraform.required_providers, run terraform init, and commit the resulting .terraform.lock.hcl when you want repeatable provider installation.
  3. Generate JSON with a real serializer: Do not assemble braces and quotes by hand. Let your language emit valid JSON so numbers, booleans, arrays, and escaping all stay correct.
  4. Prefer predictable output: Stable key ordering, stable file naming, and a root "//" marker make generated modules much easier to diff and review.
  5. Validate exactly the way Terraform will run: A generated file is not finished until it passes formatting, validation, and at least one real plan in CI.
  6. Do not rely on terraform fmt for format conversion: terraform fmt formats Terraform configuration files, including .tf.json, but it is not a general HCL-to-JSON or JSON-to-HCL converter.

Practical Validation Loop

terraform init
terraform fmt -check -recursive
terraform validate
terraform plan

Common Mistakes And Troubleshooting

  • required_providers in the wrong place: It must live inside the top-level terraform block, not inside the top-level provider object.
  • Wrong alias reference syntax: Use "provider": "aws.west", not "provider": "${aws.west}".
  • Literal versus expression confusion: module.source is literal text, while many resource arguments are expression positions. Generated JSON has to respect that difference.
  • Overusing JSON syntax: If the real problem is generating an IAM policy, ECS definition, or API payload, keep the Terraform module in HCL and use jsonencode() for that value instead.
  • Assuming validate works before init: In fresh environments, run terraform init first so provider plugins and module dependencies are available.

Conclusion

Terraform JSON configuration is most useful when machines generate it and humans validate it. If your main question is provider syntax, remember the split: declare providers in terraform.required_providers, configure them in top-level provider blocks, and select specific instances with literal provider addresses like aws.west.

That keeps .tf.json files closer to current Terraform behavior, easier to review, and less likely to break when they reach terraform init, validate, or plan.

Need help with your JSON?

Try our JSON Formatter tool to automatically identify and fix syntax errors in your JSON. JSON Formatter tool