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
- 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.
- If the content is valid JSON but a different schema, identify which Terraform version wrote it and align versions across the team.
- Verify the SSE-C customer key is correct if encryption is enabled — wrong keys yield unreadable bytes.
- Delete the corrupt lock object directly via the AWS CLI to clear the lock, then re-run the apply to acquire a fresh lock.
- 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
- Never hand-edit the `.tflock` object; use `terraform force-unlock` to clear locks.
- Keep Terraform versions aligned across operators to avoid LockInfo schema drift.
- Keep SSE-C customer keys stable so encrypted lock files decode correctly.
- Audit S3 access logs if a non-Terraform actor is writing to the lock key.
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
- failed to delete the lock file
- failed to read the body of the S3 object
- failed to unlock both S3 and DynamoDB: S3 error
- failed to unlock S3
- lock ID ' ' does not match the existing lock ID
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)