Terraform JSON: jsonencode, jsondecode, .tf.json & tfstate
Last updated:
Terraform supports JSON in two ways: jsonencode() / jsondecode() built-in functions for embedding JSON within HCL, and .tf.json files as a complete JSON-format alternative to .tf (HCL) files — both approaches produce identical infrastructure. jsonencode({ key = "value" }) converts a Terraform expression to a JSON string — the primary use case is AWS IAM policy documents, which must be JSON strings, not HCL objects. jsondecode(string) converts a JSON string back to a Terraform value; use it to parse outputs from data sources or remote state.
This guide covers jsonencode/jsondecode syntax, JSON-format .tf.json files, terraform.tfstate JSON structure and sensitive data handling, AWS IAM policy JSON patterns, data source outputs as JSON, and local_file resources for writing JSON files.
jsonencode() and jsondecode(): Built-in JSON Functions
jsonencode() and jsondecode() are Terraform built-in functions available in all versions since 0.12. jsonencode() accepts any Terraform value — object, list, map, string, number, bool, or null — and returns a compact JSON string. jsondecode() accepts a JSON string and returns the corresponding Terraform value, which you can then reference with dot notation (local.parsed.field) or index notation (local.parsed[0]).
# jsonencode() — convert HCL expression to JSON string
locals {
# Simple object
simple_json = jsonencode({ key = "value", count = 3 })
# Result: {"count":3,"key":"value"}
# Nested object with list
config_json = jsonencode({
environment = var.environment
regions = ["us-east-1", "us-west-2"]
settings = {
debug = false
timeout = 30
}
})
# Reference other resources inside jsonencode()
bucket_policy_json = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "s3:GetObject"
Resource = "${aws_s3_bucket.example.arn}/*"
Principal = "*"
}]
})
}
# jsondecode() — parse JSON string to Terraform value
locals {
# Decode a JSON string variable
config = jsondecode(var.config_json)
}
# Access decoded fields with dot notation
output "db_host" {
value = local.config.database_host
}
# jsondecode() with a list — access with index
locals {
servers = jsondecode(var.servers_json)
# var.servers_json = '["web-01","web-02","web-03"]'
}
output "first_server" {
value = local.servers[0] # "web-01"
}
# Round-trip: encode then decode (useful for deep merging)
locals {
base = { region = "us-east-1", tags = { env = "prod" } }
encoded = jsonencode(local.base)
decoded = jsondecode(local.encoded)
}
# Type checking: jsonencode() accepts any Terraform type
output "examples" {
value = {
null_json = jsonencode(null) # "null"
bool_json = jsonencode(true) # "true"
number_json = jsonencode(42) # "42"
list_json = jsonencode(["a", "b"]) # '["a","b"]'
map_json = jsonencode({ x = 1 }) # '{"x":1}'
}
}A key difference between jsonencode() and HCL heredoc JSON (<<EOF ... EOF): jsonencode() is validated at plan time — if you reference a resource attribute that does not exist or has the wrong type, Terraform reports an error before making any changes. Heredoc JSON strings are opaque to Terraform's type system and fail only at apply time when the provider validates the policy string. Prefer jsonencode() for all inline JSON generation in Terraform.
AWS IAM Policy JSON with jsonencode()
AWS IAM policy documents must be JSON strings — the AWS provider's policy argument is type string, not an HCL object. jsonencode() is the recommended approach for inline policies because it keeps the policy definition next to the resource, validates references at plan time, and avoids heredoc escaping issues. For reusable or complex policies, the aws_iam_policy_document data source is an alternative that provides additional validation.
# IAM role with inline assume-role policy using jsonencode()
resource "aws_iam_role" "lambda_exec" {
name = "lambda-execution-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "sts:AssumeRole"
Principal = { Service = "lambda.amazonaws.com" }
}]
})
}
# Inline role policy — S3 read access
resource "aws_iam_role_policy" "s3_read" {
name = "s3-read-policy"
role = aws_iam_role.lambda_exec.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "AllowS3Read"
Effect = "Allow"
Action = ["s3:GetObject", "s3:ListBucket"]
Resource = [
aws_s3_bucket.data.arn,
"${aws_s3_bucket.data.arn}/*",
]
},
{
Sid = "DenyDeleteObject"
Effect = "Deny"
Action = "s3:DeleteObject"
Resource = "${aws_s3_bucket.data.arn}/*"
}
]
})
}
# Managed policy — reusable across multiple roles
resource "aws_iam_policy" "ecr_push" {
name = "ecr-push-policy"
description = "Allow pushing images to ECR"
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = [
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:InitiateLayerUpload",
"ecr:UploadLayerPart",
"ecr:CompleteLayerUpload",
"ecr:PutImage",
]
Resource = "*"
}]
})
}
# Using locals to build complex policies with conditions
locals {
ip_allowlist = ["203.0.113.0/24", "198.51.100.0/24"]
s3_policy = {
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = ["s3:GetObject"]
Resource = "${aws_s3_bucket.data.arn}/*"
Condition = {
IpAddress = {
"aws:SourceIp" = local.ip_allowlist
}
}
}
]
}
}
resource "aws_s3_bucket_policy" "restricted" {
bucket = aws_s3_bucket.data.id
policy = jsonencode(local.s3_policy)
}AWS IAM policy documents are subject to a 6,144-character size limit for inline policies and a 10,240-character limit for managed policies. If your jsonencode() output exceeds these limits, split the policy into multiple statements across separate managed policies and attach them to the role. Use terraform plan output to see the exact JSON that will be sent to AWS — the JSON is shown in the plan diff, making it easy to verify policy correctness before applying.
JSON-Format .tf.json Files: HCL Alternative
Every HCL construct in Terraform has an exact JSON equivalent. Terraform processes .tf and .tf.json files in the same directory identically — they can coexist and reference each other freely. The primary use case for .tf.json is programmatic Terraform generation from other tools, since JSON is far easier to produce than HCL from most programming languages. Terraform CDK (cdktf) and internal developer platforms commonly emit .tf.json files.
// main.tf.json — JSON equivalent of main.tf
// Every HCL block type has a JSON mapping
{
"terraform": {
"required_providers": {
"aws": {
"source": "hashicorp/aws",
"version": "~> 5.0"
}
}
},
"provider": {
"aws": {
"region": "${var.aws_region}"
}
},
"variable": {
"aws_region": {
"type": "string",
"default": "us-east-1"
},
"environment": {
"type": "string"
}
},
"locals": {
"common_tags": {
"Environment": "${var.environment}",
"ManagedBy": "Terraform"
}
},
"resource": {
"aws_s3_bucket": {
"example": {
"bucket": "my-bucket-${var.environment}",
"tags": "${local.common_tags}"
}
},
"aws_instance": {
"web": {
"ami": "ami-0c02fb55956c7d316",
"instance_type": "t3.micro",
"tags": "${merge(local.common_tags, { Name = "web-server" })}"
}
}
},
"output": {
"bucket_name": {
"value": "${aws_s3_bucket.example.bucket}",
"description": "The name of the S3 bucket"
},
"instance_ip": {
"value": "${aws_instance.web.public_ip}"
}
},
"data": {
"aws_ami": {
"ubuntu": {
"most_recent": true,
"owners": ["099720109477"],
"filter": [
{
"name": "name",
"values": ["ubuntu/images/hvm-ssd/ubuntu-*-22.04-amd64-server-*"]
}
]
}
}
}
}In JSON-format files, Terraform expression syntax (${...}) works the same as in HCL — use it for variable references, function calls, and resource attribute access. Meta-arguments like count, for_each, depends_on, lifecycle, and provider are all supported in .tf.json. The main limitation: JSON does not support comments, making .tf.json harder to annotate than HCL. For hand-written Terraform, use .tf; for generated Terraform, use .tf.json.
terraform.tfstate: JSON Structure and Sensitive Data
terraform.tfstate is a plain JSON file that records the current state of all managed resources. Terraform uses it to calculate diffs during terraform plan — comparing the actual state of resources (as last fetched from providers) against the desired configuration. Understanding its structure is essential for debugging, state surgery, and managing sensitive data that Terraform stores in state.
// terraform.tfstate — top-level structure (version 4)
{
"version": 4,
"terraform_version": "1.9.5",
"serial": 23,
"lineage": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"outputs": {
"bucket_name": {
"value": "my-bucket-prod",
"type": "string",
"sensitive": false
},
"db_password": {
"value": "super-secret-password",
"type": "string",
"sensitive": true
}
},
"resources": [
{
"module": "module.networking",
"mode": "managed",
"type": "aws_vpc",
"name": "main",
"provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
"instances": [
{
"schema_version": 0,
"attributes": {
"id": "vpc-0abc123def456789",
"cidr_block": "10.0.0.0/16",
"enable_dns_support": true,
"tags": { "Environment": "prod" }
},
"sensitive_attributes": [],
"private": "base64-encoded-provider-private-state",
"dependencies": ["aws_internet_gateway.main"]
}
]
},
{
"mode": "data",
"type": "aws_ami",
"name": "ubuntu",
"provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
"instances": [
{
"schema_version": 0,
"attributes": {
"id": "ami-0c02fb55956c7d316",
"name": "ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-20240301",
"most_recent": true
},
"sensitive_attributes": []
}
]
}
]
}
# Safe state manipulation commands — NEVER edit tfstate JSON directly
terraform state list # list all resources in state
terraform state show aws_vpc.main # show a specific resource
terraform state mv aws_vpc.main aws_vpc.prod # rename/move a resource
terraform state rm aws_s3_bucket.old # remove from state (does not destroy)
terraform import aws_s3_bucket.new my-bucket # import existing resource
# Remote state — store tfstate in S3 (recommended for teams)
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-state-lock"
encrypt = true
}
}Sensitive data in terraform.tfstate is stored in plaintext — database passwords, API keys, and private keys all appear as plain strings in the attributes object. Mark outputs as sensitive = true to prevent them from appearing in plan/apply output, but this does not encrypt the state file itself. Use a remote backend with encryption at rest (S3 with KMS, Terraform Cloud, or HCP Terraform) to protect sensitive state. The serial field increments on every state change and is used for conflict detection with remote state — if two operators apply simultaneously, the one with the lower serial loses and must re-plan.
Reading External JSON with file() and jsondecode()
Combining file() and jsondecode() lets you read external JSON files into Terraform values, separating data from infrastructure code. This pattern is useful for large IAM policies, environment-specific configurations, and data-driven resource creation with for_each. Always use path.module in file() calls to ensure the path resolves correctly when the module is called from a different working directory.
# Read an IAM policy from an external JSON file
# File: iam/s3-read-policy.json
# {
# "Version": "2012-10-17",
# "Statement": [{
# "Effect": "Allow",
# "Action": ["s3:GetObject", "s3:ListBucket"],
# "Resource": "*"
# }]
# }
resource "aws_iam_policy" "s3_read" {
name = "s3-read-policy"
policy = file("${path.module}/iam/s3-read-policy.json")
# file() returns the raw string — aws_iam_policy.policy accepts JSON string directly
}
# jsondecode() — access fields from an external JSON config
locals {
app_config = jsondecode(file("${path.module}/config/app.json"))
# app.json: {"database":{"host":"db.prod","port":5432},"cache":{"ttl":300}}
}
resource "aws_ssm_parameter" "db_host" {
name = "/myapp/db/host"
type = "String"
value = local.app_config.database.host # "db.prod"
}
# Data-driven resource creation with for_each
# File: environments/buckets.json
# {"buckets": [
# {"name": "assets", "versioning": true},
# {"name": "logs", "versioning": false},
# {"name": "backup", "versioning": true}
# ]}
locals {
bucket_config = jsondecode(file("${path.module}/environments/buckets.json"))
buckets = { for b in local.bucket_config.buckets : b.name => b }
}
resource "aws_s3_bucket" "app" {
for_each = local.buckets
bucket = "${var.environment}-${each.key}"
}
resource "aws_s3_bucket_versioning" "app" {
for_each = local.buckets
bucket = aws_s3_bucket.app[each.key].id
versioning_configuration {
status = each.value.versioning ? "Enabled" : "Suspended"
}
}
# Read JSON from AWS SSM Parameter Store (runtime data source)
data "aws_ssm_parameter" "config" {
name = "/myapp/${var.environment}/config"
with_decryption = true
}
locals {
ssm_config = jsondecode(data.aws_ssm_parameter.config.value)
}
# Read JSON from AWS Secrets Manager
data "aws_secretsmanager_secret_version" "db_creds" {
secret_id = "prod/myapp/db"
}
locals {
db = jsondecode(data.aws_secretsmanager_secret_version.db_creds.secret_string)
}
output "db_username" {
value = local.db.username
sensitive = true
}When using file() with for_each, Terraform reads the file during the plan phase — the file must exist before terraform plan runs, not just before terraform apply. If the JSON file is generated by another Terraform configuration, use terraform_remote_state or store it in SSM/Secrets Manager instead of relying on a file on disk. For JSON files with sensitive content, use sensitive(jsondecode(file(...))) to mark the entire decoded value as sensitive and prevent it from appearing in logs.
Writing JSON Files with local_file Resource
The local_file resource (from the hashicorp/local provider) writes files to the local filesystem during terraform apply. It is commonly used to generate kubeconfig files after creating a Kubernetes cluster, write application config files after provisioning infrastructure, and produce JSON manifests for other tools. The file is deleted when you run terraform destroy.
# Configure the local provider
terraform {
required_providers {
local = {
source = "hashicorp/local"
version = "~> 2.5"
}
}
}
# Write a JSON config file after provisioning
resource "local_file" "app_config" {
filename = "${path.module}/output/app-config.json"
file_permission = "0644"
content = jsonencode({
environment = var.environment
api = {
url = "https://${aws_lb.main.dns_name}"
timeout = 30
}
database = {
host = aws_db_instance.main.address
port = aws_db_instance.main.port
name = aws_db_instance.main.db_name
}
cache = {
endpoint = aws_elasticache_cluster.main.cache_nodes[0].address
port = 6379
}
version = "1.0.0"
region = var.aws_region
})
}
# local_sensitive_file — for files containing secrets
resource "local_sensitive_file" "kubeconfig" {
filename = "${path.module}/output/kubeconfig.json"
file_permission = "0600" # restrictive permissions, treated as sensitive in plan
content = jsonencode({
apiVersion = "v1"
kind = "Config"
clusters = [{
name = aws_eks_cluster.main.name
cluster = {
server = aws_eks_cluster.main.endpoint
certificate-authority-data = aws_eks_cluster.main.certificate_authority[0].data
}
}]
users = [{
name = "admin"
user = {
token = data.aws_eks_cluster_auth.main.token
}
}]
contexts = [{
name = aws_eks_cluster.main.name
context = {
cluster = aws_eks_cluster.main.name
user = "admin"
}
}]
current-context = aws_eks_cluster.main.name
})
}
# Write multiple environment config files using for_each
variable "environments" {
default = ["dev", "staging", "prod"]
}
resource "local_file" "env_configs" {
for_each = toset(var.environments)
filename = "${path.module}/output/${each.key}-config.json"
file_permission = "0644"
content = jsonencode({
env = each.key
log_level = each.key == "prod" ? "warn" : "debug"
replicas = each.key == "prod" ? 3 : 1
})
}
# Depend on local_file to ensure it is written before other steps
resource "null_resource" "deploy" {
triggers = {
config_hash = local_file.app_config.content_sha256
}
provisioner "local-exec" {
command = "deploy.sh ${local_file.app_config.filename}"
}
depends_on = [local_file.app_config]
}local_file resources are tracked in terraform.tfstate like any other resource — Terraform detects if the file content changes and replaces it on the next apply. Use content_sha256 (available as an attribute on the resource after creation) to trigger downstream resources when the JSON file changes. Avoid writing secrets to local_file — use local_sensitive_file and ensure the output directory is in .gitignore. For CI/CD environments where the filesystem is ephemeral, prefer storing generated configs in SSM Parameter Store or S3 instead of local files.
JSON in Terraform Modules and Variable Definitions
Terraform modules pass JSON as string variables or complex type variables. When a module accepts a JSON string, callers use jsonencode() to construct it; when it accepts a complex type (object, list, map), callers pass HCL directly. Understanding which approach a module uses — and declaring appropriate types in variable blocks — determines how JSON flows through your module hierarchy.
# Module: modules/iam-role/variables.tf
# Approach 1: accept complex type — no JSON needed in the module
variable "policy_statements" {
description = "List of IAM policy statement objects"
type = list(object({
effect = string
actions = list(string)
resources = list(string)
conditions = optional(list(object({
test = string
variable = string
values = list(string)
})), [])
}))
}
# Module generates the JSON internally
resource "aws_iam_role_policy" "this" {
name = "${var.role_name}-policy"
role = aws_iam_role.this.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [for stmt in var.policy_statements : {
Effect = stmt.effect
Action = stmt.actions
Resource = stmt.resources
Condition = length(stmt.conditions) > 0 ? {
for cond in stmt.conditions :
cond.test => { (cond.variable) = cond.values }
} : null
}]
})
}
# Caller passes HCL — no jsonencode() at the call site
module "lambda_role" {
source = "./modules/iam-role"
role_name = "lambda-exec"
policy_statements = [
{
effect = "Allow"
actions = ["s3:GetObject", "s3:ListBucket"]
resources = [aws_s3_bucket.data.arn, "${aws_s3_bucket.data.arn}/*"]
conditions = []
}
]
}
# Approach 2: accept a JSON string — module receives pre-encoded JSON
variable "custom_policy_json" {
description = "Custom IAM policy document as JSON string"
type = string
default = null
}
# Variable definitions file: terraform.tfvars.json
# {
# "aws_region": "us-east-1",
# "environment": "prod",
# "instance_count": 3,
# "tags": {
# "Team": "platform",
# "CostCenter": "engineering"
# }
# }
# Environment variable for JSON complex type
# export TF_VAR_tags='{"Team":"platform","CostCenter":"engineering"}'
# Passing JSON via command line (object type)
# terraform apply -var='tags={"Team":"platform","CostCenter":"engineering"}'
# Variable with validation — ensure JSON is valid
variable "user_data_json" {
type = string
description = "EC2 user data script config as JSON string"
validation {
condition = can(jsondecode(var.user_data_json))
error_message = "The user_data_json variable must be a valid JSON string."
}
}
# Decode in locals to access fields
locals {
user_data = jsondecode(var.user_data_json)
}
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = local.user_data.instance_type
user_data = templatefile("${path.module}/user_data.sh.tpl", {
packages = join(" ", local.user_data.packages)
})
}The can() function combined with jsondecode() in a variable validation block provides a lightweight JSON schema check — can(jsondecode(var.json_string)) returns true if the string is valid JSON and false otherwise, producing a clear error message at terraform plan time instead of a cryptic provider error at apply time. For module interfaces, prefer strongly-typed object() variables over raw JSON strings when possible — Terraform validates types at plan time and provides better error messages. Use JSON strings only when the downstream resource requires them (IAM policies, S3 bucket policies) or when the schema is dynamic and cannot be expressed with a fixed object() type.
Key Terms
- jsonencode()
- A Terraform built-in function that converts any Terraform value — object, list, map, string, number, bool, or null — to a compact JSON string. Available since Terraform 0.12. The primary use case is generating AWS IAM policy documents, S3 bucket policies, and other AWS resources that require JSON strings rather than HCL objects. The function validates its input at plan time, catching type errors and invalid references before any infrastructure changes are made. Output is always compact JSON (no whitespace). Accepts nested objects and lists, and correctly handles all Terraform types including sets and tuples.
- jsondecode()
- A Terraform built-in function that parses a JSON string and returns the corresponding Terraform value. JSON objects become Terraform object values (accessible with dot notation), JSON arrays become Terraform list or tuple values (accessible with index notation), and JSON primitives become their Terraform equivalents. Commonly used with
file()to read external JSON configuration files, withdatasources that return JSON strings (SSM Parameter Store, Secrets Manager), and with Terraform remote state outputs. If the input is not valid JSON, Terraform reports an error during the plan phase. - .tf.json file
- A JSON-format Terraform configuration file that is functionally equivalent to a
.tf(HCL) file. Terraform reads and merges all.tfand.tf.jsonfiles in a directory during initialization. Every HCL construct — resources, variables, outputs, locals, data sources, provider blocks, terraform blocks — has a JSON equivalent. The file extension must be exactly.tf.json; other extensions are ignored. Primary use case: programmatic Terraform generation from CI/CD pipelines, internal developer platforms, and tools like Terraform CDK (cdktf) that emit JSON rather than HCL. Does not support HCL comments (use JSON-format files only for generated configs). - terraform.tfstate
- The JSON file that records the current state of all Terraform-managed resources. Terraform uses it to compute diffs during
terraform planby comparing the stored state against the desired configuration and the actual state of resources. Contains the top-level fieldsversion(format version, currently 4),terraform_version,serial(increments on every change, used for conflict detection),lineage(UUID identifying this state lineage),outputs, andresources. Never edit manually — useterraform statesubcommands. Sensitive attributes are stored in plaintext; protect state files with encrypted remote backends. - HCL (HashiCorp Configuration Language)
- The domain-specific language used in
.tffiles for writing Terraform configurations. HCL is designed to be human-readable and writable, with syntax closer to JSON than to general-purpose programming languages. Features include block syntax for resource definitions, expression language for references and function calls, heredoc strings, and conditional expressions. HCL and JSON are interchangeable in Terraform —.tf.jsonfiles use JSON syntax to express the same constructs. HCL supports comments (#and//for single-line,/* */for block) while JSON does not. For hand-written configurations, HCL is preferred; for generated configurations, JSON is easier to produce programmatically. - local_file resource
- A Terraform resource provided by the
hashicorp/localprovider that creates and manages a file on the local filesystem where Terraform runs. Arguments includefilename(path to write),content(file content as a string), andfile_permission(Unix permissions, default0777). The file is created duringterraform applyand deleted duringterraform destroy. Commonly used to write generated JSON config files, kubeconfig files, and scripts after infrastructure is provisioned. Uselocal_sensitive_filefor files containing secrets — it sets restrictive permissions (0700) and marks the resource as sensitive in plan output.
FAQ
How do I use jsonencode() in Terraform?
Call jsonencode() with any Terraform expression as its argument — the function converts it to a compact JSON string. Basic example: jsonencode({ key = "value", count = 3 }) produces {"count":3,"key":"value"}. For AWS IAM policies, pass a full policy object: jsonencode({ Version = "2012-10-17", Statement = [{ Effect = "Allow", Action = "s3:*", Resource = "*" }] }). You can reference other Terraform values inside the expression — jsonencode({ bucket = aws_s3_bucket.example.id }) — and Terraform validates these references at plan time. Assign the result to any argument that accepts a JSON string, such as policy on aws_iam_role_policy or aws_iam_policy. Use a locals block to define complex policy structures before encoding for readability.
How do I embed a JSON policy in Terraform?
Use jsonencode() directly in the policy argument of aws_iam_role_policy, aws_iam_policy, or aws_s3_bucket_policy. Pattern: policy = jsonencode({ Version = "2012-10-17" Statement = [{ Effect = "Allow" Action = ["s3:GetObject"] Resource = aws_s3_bucket.example.arn }] }). For complex policies, define the structure in a locals block first: locals { policy = { Version = "2012-10-17", Statement = [...] } } then reference it as policy = jsonencode(local.policy). The advantage over heredoc JSON strings is plan-time validation of resource references and type checking. The aws_iam_policy_document data source is an alternative that offers additional validation features, including automatic deduplication of statements.
What is a .tf.json file in Terraform?
A .tf.json file is a JSON-format Terraform configuration that is fully equivalent to a .tf (HCL) file — Terraform processes both formats identically and merges them in the same directory. Every HCL block type maps to a JSON key: "resource": {"aws_s3_bucket": {"example": {"bucket": "my-bucket" } } } is the JSON equivalent of resource "aws_s3_bucket" "example" { bucket = "my-bucket" }. The primary use case is programmatic Terraform generation — CI/CD pipelines, Terraform CDK, and internal developer platforms emit .tf.json because JSON is easier to produce than HCL from code. The file extension must be exactly .tf.json. JSON does not support comments, so use .tf.json only for generated files, not hand-written configurations.
How do I read a JSON file in Terraform?
Combine file() and jsondecode(): jsondecode(file("${}path.module}/config.json")). Store the result in a locals block to reuse it: locals { config = jsondecode(file("${}path.module}/config.json")) }, then access fields as local.config.database_host. Always use path.module (not a bare relative path like ./config.json) so the path resolves correctly when the module is called from any working directory. If the file is used for an AWS resource that accepts raw JSON strings (like aws_iam_policy.policy), you can skip jsondecode() and pass file() directly — the file content is a ready-to-use JSON string. For data-driven resource creation, use the decoded value with for_each: for_each = { for item in local.config.items : item.name => item }.
What is the structure of terraform.tfstate?
terraform.tfstate is a JSON file with these top-level keys: version (state format version, currently 4), terraform_version (Terraform CLI version that last wrote the file), serial (integer incrementing on every state change, used for conflict detection), lineage (UUID stable across the state file's lifetime), outputs (map of output names to their values and types), and resources (array of resource objects). Each resource object contains mode ("managed" or "data"), type, name, provider, and instances — an array of instance objects with attributes (all resource attributes as key-value pairs) and dependencies. Sensitive attribute values are stored in plaintext in the attributes object — always use an encrypted remote backend in production.
How do I decode JSON output in Terraform?
Use jsondecode() on any string that contains JSON — data source outputs, SSM parameters, Secrets Manager secrets, or remote state outputs. For SSM Parameter Store: locals { config = jsondecode(data.aws_ssm_parameter.config.value) } then reference fields as local.config.database_host. For Secrets Manager: locals { creds = jsondecode(data.aws_secretsmanager_secret_version.db.secret_string) } then use local.creds.username and local.creds.password. For remote Terraform state with a JSON output: locals { remote = jsondecode(data.terraform_remote_state.shared.outputs.config_json) }. Mark decoded values containing secrets as sensitive: sensitive(jsondecode(...)). For Terraform remote state outputs that are already typed (not JSON strings), use the output values directly without jsondecode().
How do I write a JSON file with Terraform?
Use the local_file resource from the hashicorp/local provider. First, add it to your required_providers: local = { source = "hashicorp/local", version = "~> 2.5" }. Then define the resource: resource "local_file" "config" { filename = "${}path.module}/output/config.json", file_permission = "0644", content = jsonencode({ api_url = "https://${}aws_lb.main.dns_name}" }) }. For files with sensitive content (credentials, tokens), use local_sensitive_file instead — it sets 0700 permissions and marks the resource as sensitive in plan output. The file is created on terraform apply and deleted on terraform destroy. Add the output directory to .gitignore to avoid accidentally committing generated files.
How do I pass JSON as a Terraform variable?
For complex types, declare a typed object() or list() variable and pass HCL in terraform.tfvars or a .tfvars.json file. In terraform.tfvars.json: {"tags": {"env": "prod", "team": "platform"} } — Terraform auto-loads this file. Via environment variable: export TF_VAR_tags='{"env":"prod"}'. Via command line: terraform apply -var='tags={"env":"prod"}'. For a variable declared as type = string that should hold a JSON string, pass the JSON directly and decode it inside the config with jsondecode(var.policy_json). Add validation to ensure the JSON is valid: validation { condition = can(jsondecode(var.policy_json)) error_message = "Must be valid JSON." }. Prefer typed variables over JSON strings for module interfaces — they provide better plan-time error messages.
Further reading and primary sources
- Terraform Docs: jsonencode Function — Official Terraform reference for jsonencode() with syntax and examples
- Terraform Docs: jsondecode Function — Official Terraform reference for jsondecode() with type mapping details
- Terraform Docs: JSON Configuration Syntax — Complete guide to .tf.json file format and HCL-to-JSON mappings
- Terraform Docs: State — Terraform state file purpose, structure, and remote backend configuration
- hashicorp/local Provider: local_file — local_file and local_sensitive_file resource reference documentation