Skip to content

fix: metadata key deletion broken on user and organization updates #18

Description

@sionsmith

Bug

When updating a workos_user or workos_organization resource and removing metadata keys, the removed keys persist on the WorkOS side. This causes Terraform to report:

Error: Provider produced inconsistent result after apply

When applying changes to workos_user.test, provider produced an unexpected
new value: .metadata: new element "team" has appeared.

Root cause

The WorkOS API merges metadata on PUT updates rather than replacing it. To delete a key, the API requires sending null for that key's value (docs). The provider currently sends only the new keys using map[string]string, which cannot represent null — so removed keys are never communicated to the API.

Affected resources

  • workos_userresource_user.go Update method (line ~473)
  • workos_organizationresource_organization.go Update method (line ~354)

Failing test

TestAccUserResource_withExternalIDAndMetadata step 3/3 — updates metadata from {department: "Engineering", team: "Platform"} to {department: "Product", role: "Lead"}. The "team" key persists because the provider never tells the API to delete it.

Note: This test was previously masked by a different failure (the IsUnknown() crash fixed in #16). The SHA-pinning commit also failed CI for the same underlying reason.

Proposed fix

1. Change update request structs to support null values

In internal/client/models.go, change the Metadata field on update request structs from map[string]string to map[string]*string:

// UserUpdateRequest
Metadata map[string]*string `json:"metadata,omitempty"`

// OrganizationUpdateRequest
Metadata map[string]*string `json:"metadata,omitempty"`

Create request structs remain map[string]string — no null values needed on create.

2. Compute key deletions in provider Update methods

In both resource_user.go and resource_organization.go Update methods, compare old vs new metadata and explicitly set removed keys to nil:

// Build update map with pointer values
updateMap := make(map[string]*string, len(newMetadata))
for k, v := range newMetadata {
    v := v
    updateMap[k] = &v
}
// Null out removed keys so WorkOS deletes them
if !state.Metadata.IsNull() && !state.Metadata.IsUnknown() {
    oldMetadata := make(map[string]string)
    state.Metadata.ElementsAs(ctx, &oldMetadata, false)
    for key := range oldMetadata {
        if _, exists := newMetadata[key]; !exists {
            updateMap[key] = nil // serializes as JSON null
        }
    }
}
updateReq.Metadata = updateMap

3. Handle metadata fully removed (set → null)

Edge case: if a user removes the entire metadata block from their config, plan.Metadata is null. Currently ElementsAs on a null value won't produce a usable map, so old keys persist silently. The fix should detect this case and send all old keys as null:

if plan.Metadata.IsNull() && !state.Metadata.IsNull() {
    // All metadata removed — null out every old key
    oldMetadata := make(map[string]string)
    state.Metadata.ElementsAs(ctx, &oldMetadata, false)
    updateMap := make(map[string]*string, len(oldMetadata))
    for key := range oldMetadata {
        updateMap[key] = nil
    }
    updateReq.Metadata = updateMap
}

Files to change

File Change
internal/client/models.go UserUpdateRequest.Metadata and OrganizationUpdateRequest.Metadatamap[string]*string
internal/provider/resource_user.go Update method: compute key deletions + handle full removal
internal/provider/resource_organization.go Update method: same pattern as user resource

Test plan

  • TestAccUserResource_withExternalIDAndMetadata passes (step 3 — key removal)
  • Add test: organization metadata key removal on update
  • Add test: user metadata fully removed (set → null)
  • Add test: organization metadata fully removed (set → null)
  • go build ./... passes
  • All existing acceptance tests pass

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions