Azure / Azure/terraform-provider-azapi
`list_unique_id_property` is silently ignored when the identifier is not a string
- Dominant language
- Go
- Stars
- 244
- Forks
- 97
- Avg merge
- 5d 9h
- Merged PRs (30d)
- 9
Description
**azapi version:** 2.12.0 (also present on `main` at time of writing)
**Terraform version:** 1.15.8
## Summary
`list_unique_id_property` silently has no effect when the nominated property is not a JSON string. The provider falls back to comparing the list by position, which produces a permanent diff for any list ARM returns in an order different from the one the practitioner sends.
The same defect also disables `ignore_other_items_in_list` for the same lists, because that option is only consulted inside the branch that requires a usable identifier.
There is no warning, no error and no debug output — the option simply does nothing.
## Root cause
`identifierOfArrayItemByKey` in `utils/json.go` only accepts an identifier that type-asserts to a Go `string`:
```go
value := ""
if v, exists := inputMap[k]; exists && v != nil {
if strVal, ok := v.(string); ok {
value = strVal
}
}
if value != "" {
hasNonEmpty = true
}
```
Any JSON number unmarshals to `float64` and any JSON boolean to `bool`, so the assertion fails, `value` stays empty, `hasNonEmpty` stays false and the function returns `""`.
`updateObjectAtPath` then treats the list as having no identifier:
```go
identifierKey := listIdentifierKeyForPath(path, option)
hasIdentifier := identifierOfArrayItemByKey(oldValue[0], identifierKey) != "" &&
identifierOfArrayItemByKey(newArr[0], identifierKey) != ""
if !hasIdentifier {
if len(oldValue) != len(newArr) {
return newArr
}
res := make([]interface{}, 0)
for index := range oldValue {
res = append(res, updateObjectAtPath(oldValue[index], newArr[index], option, path))
}
return res
}
```
Because that branch returns early, the `ignoreOthers` check further down is unreachable whenever the identifier is unusable:
```go
ignoreOthers := option.IgnoreOtherItemsInList != nil && option.IgnoreOtherItemsInList[path]
```
So one type assertion accounts for two separate symptoms.
## Reproduction
`Microsoft.Compute/virtualMachines` is a good case because `properties.storageProfile.dataDisks` is keyed by `lun`, which is a number, and ARM returns the array in the order the disks were attached rather than the order it was sent.
Attach three data disks so that the attachment order differs from the lun order, then configure:
```hcl
resource "azapi_resource" "vm" {
type = "Microsoft.Compute/virtualMachines@2024-11-01"
# ...
list_unique_id_property = {
"properties.storageProfile.dataDisks" = "lun"
}
}
```
ARM returns:
```json
[
{ "lun": 1, "name": "dsk-caller-owned" },
{ "lun": 2, "name": "dsk-two" },
{ "lun": 0, "name": "dsk-one" }
]
```
while the configuration sends `lun` 0, 1, 2. Every subsequent plan reports a change on `body`, and the diff is purely positional — the entries are identical, only their index differs:
```
BEFORE luns: 1,2,0
AFTER luns: 0,1,2
```
Applying does not converge; the next plan shows the same diff.
Changing the identifier to `name`, which ARM also returns and which is a string, makes the matching work and the plan is clean against the same unchanged ARM ordering. That is the workaround, but it is only available when the resource happens to expose a string property that is unique per entry.
## Impact
Any list whose natural key is numeric or boolean cannot use this feature. Beyond `dataDisks`, the same shape appears in load balancer and application gateway sub-resources keyed by port, and in rule collections keyed by priority.
The silence is the worst part: `list_unique_id_property` is exactly the option a practitioner reaches for to fix an ordering diff, and when the key is numeric it appears to be accepted while changing nothing.
## Suggested fix
Accept the other scalar JSON types in `identifierOfArrayItemByKey`, since the value is only used as a comparison token:
```go
if v, exists := inputMap[k]; exists && v != nil {
switch t := v.(type) {
case string:
value = t
case float64:
value = strconv.FormatFloat(t, 'f', -1, 64)
case bool:
value = strconv.FormatBool(t)
case json.Number:
value = t.String()
}
}
```
Composite keys keep working, because the parts are already joined with `_` after extraction.
`utils/json_test.go` already covers this function and would be the natural home for a case with a numeric key. Happy to open a PR if the approach looks right.
Contributor guide
Research direction
Start in utils/json.go at identifierOfArrayItemByKey, then read its existing coverage in utils/json_test.go. Add coverage for numeric and boolean identifiers and verify that uniquely keyed lists avoid positional diffs and still honor ignore_other_items_in_list; run the relevant JSON utility tests.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- azure, go, terraform
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 86/100