hashicorp/terraform · error

failed to unmarshal JSON data into LockInfo struct

Error message

failed to unmarshal JSON data into LockInfo struct: %w

What it means

Thrown inside unlockWithFile when the lock file body cannot be JSON-decoded into statemgr.LockInfo. The GetObject and body read both succeeded, but the bytes are not valid JSON or do not carry the expected LockInfo fields. This blocks the ID-verification step that precedes DeleteObject.

Solutions

  1. Download the lock file and inspect it: `aws s3api get-object --bucket <bucket> --key <path>.tflock /tmp/lock.json` then view it; if corrupt, it is safe to delete since the lock is unusable.
  2. If the content is valid JSON but a different schema, identify which Terraform version wrote it and align versions across the team.
  3. Verify the SSE-C customer key is correct if encryption is enabled — wrong keys yield unreadable bytes.
  4. Delete the corrupt lock object directly via the AWS CLI to clear the lock, then re-run the apply to acquire a fresh lock.
  5. Audit bucket access logs to find what wrote the malformed object.

Example fix

# inspect then remove the corrupt lock file
aws s3api get-object --bucket tf-state-prod --key prod/terraform.tflock.tflock /tmp/lock.json
cat /tmp/lock.json   # if garbage, delete it
aws s3api delete-object --bucket tf-state-prod --key prod/terraform.tflock.tflock
Defensive patterns

Strategy: validation

Validate before calling

// Before unmarshal, sanity-check the bytes look like JSON.
func looksLikeJSON(b []byte) bool {
  s := bytes.TrimSpace(b)
  return len(s) > 0 && (s[0] == '{' || s[0] == '[')
}
// usage:
if !looksLikeJSON(data) {
  return fmt.Errorf("lock file body is not JSON (possibly wrong SSE-C key); got %q", string(data))
}

Type guard

// Validate a parsed LockInfo before trusting it.
func validLockInfo(li *statemgr.LockInfo) bool {
  return li != nil && li.ID != "" && li.Path != ""
}

Try / catch

// On unmarshal failure, fetch+log the raw bytes for diagnosis, then surface a clear error.
if err := json.Unmarshal(data, lockInfo); err != nil {
  log.Warn(fmt.Sprintf("corrupt lock file body: %q", string(data)))
  return fmt.Errorf("failed to unmarshal JSON data into LockInfo struct: %w; "+
    "the lock file may be corrupt — inspect or delete %s/%s", err, c.bucketName, c.lockFilePath)
}

Prevention

When it happens

Trigger: json.Unmarshal(data, lockInfo) at client.go:536 returns an error. Triggers: the lock file was manually edited or truncated, an older/newer Terraform wrote a different lock-file schema, the object was overwritten by a non-Terraform process, or encryption/encoding mismatch produced garbage bytes that are not JSON.

Common situations: Someone hand-edited the `.tflock` object, a Terraform version upgrade changed the LockInfo serialization, an SSE-C key mismatch yielded decrypted garbage that parses as non-JSON, or a third-party tool wrote to the lock key.

Related errors


AI-assisted analysis of hashicorp/terraform@d32a084675 (2026-08-11). Data as JSON: /api/errors/ce321d847162413b. Report an issue: GitHub.

Appendix: source

Thrown at internal/backend/remote-state/s3/client.go:537

	getOutput, err := c.s3Client.GetObject(ctx, getInput)
	if err != nil {
		return fmt.Errorf("unable to retrieve file from S3 bucket '%s' with key '%s': %w", c.bucketName, c.lockFilePath, err)
	}
	defer func() {
		if cerr := getOutput.Body.Close(); cerr != nil {
			log.Warn(fmt.Sprintf("failed to close S3 object body: %v", cerr))
		}
	}()

	data, err := io.ReadAll(getOutput.Body)
	if err != nil {
		return fmt.Errorf("failed to read the body of the S3 object: %w", err)
	}

	lockInfo := &statemgr.LockInfo{}
	if err := json.Unmarshal(data, lockInfo); err != nil {
		return fmt.Errorf("failed to unmarshal JSON data into LockInfo struct: %w", err)
	}
	lockErr.Info = lockInfo

	// Verify that the provided lock ID matches the lock ID of the retrieved lock file.
	if lockInfo.ID != id {
		return fmt.Errorf("lock ID '%s' does not match the existing lock ID '%s'", id, lockInfo.ID)
	}

	// Delete the lock file to release the lock.
	_, err = c.s3Client.DeleteObject(ctx, &s3.DeleteObjectInput{
		Bucket: aws.String(c.bucketName),
		Key:    aws.String(c.lockFilePath),
	})

	if err != nil {
		return fmt.Errorf("failed to delete the lock file: %w", err)
	}

View on GitHub (pinned to d32a084675)