This is an automated email from the ASF dual-hosted git repository.

abh1sar pushed a commit to branch fix-list-response-keys
in repository https://gitbox.apache.org/repos/asf/cloudstack-go.git

commit e847e5d262614df75197dec465761556a53565f9
Author: Abhisar Sinha <[email protected]>
AuthorDate: Tue Aug 11 17:23:01 2026 +0530

    Fix list response keys derived from the API name
    
    CloudStack keys a list response's items by the response object name, not by 
the
    API name the generator derived the tag from, so 16 list APIs decoded as a
    permanently empty slice while count parsed from the same body and no error 
was
    returned. The keys are now data in a documented listResponseKeys map rather 
than
    24 hand-written switch arms, each one observed on a real server and pinned 
by a
    test, leaving only genuine shape differences in the switch; 
listCaCertificate and
    listUsageServerMetrics also become pointers, since they return a single 
object
    with no count. Also moves the getRawValue empty-array guard from #154 into 
the
    generator, where it had been edited into the generated file only and so was
    reverted by every `make code`.
---
 cloudstack/ASNumberRangeService.go          |   2 +-
 cloudstack/BackupService.go                 |   2 +-
 cloudstack/CertificateService.go            |   3 +-
 cloudstack/ClusterService.go                |   2 +-
 cloudstack/ExtensionService.go              |   2 +-
 cloudstack/FirewallService.go               |   2 +-
 cloudstack/HostService.go                   |   2 +-
 cloudstack/NetworkService.go                |   4 +-
 cloudstack/SystemVMService.go               |   2 +-
 cloudstack/UsageService.go                  |   5 +-
 cloudstack/UserService.go                   |   2 +-
 cloudstack/VolumeService.go                 |   2 +-
 cloudstack/ZoneService.go                   |   4 +-
 generate/generate.go                        | 174 +++++++++++++-----------
 test/ListResponseJSONTagsRegression_test.go | 196 ++++++++++++++++++++++++++++
 15 files changed, 308 insertions(+), 96 deletions(-)

diff --git a/cloudstack/ASNumberRangeService.go 
b/cloudstack/ASNumberRangeService.go
index e781764..95f73c6 100644
--- a/cloudstack/ASNumberRangeService.go
+++ b/cloudstack/ASNumberRangeService.go
@@ -385,7 +385,7 @@ func (s *ASNumberRangeService) ListASNRanges(p 
*ListASNRangesParams) (*ListASNRa
 
 type ListASNRangesResponse struct {
        Count     int         `json:"count"`
-       ASNRanges []*ASNRange `json:"asnrange"`
+       ASNRanges []*ASNRange `json:"asnumberrange"`
 }
 
 type ASNRange struct {
diff --git a/cloudstack/BackupService.go b/cloudstack/BackupService.go
index 35f1f48..0abbe71 100644
--- a/cloudstack/BackupService.go
+++ b/cloudstack/BackupService.go
@@ -3544,7 +3544,7 @@ func (s *BackupService) ListBackupProviders(p 
*ListBackupProvidersParams) (*List
 
 type ListBackupProvidersResponse struct {
        Count           int               `json:"count"`
-       BackupProviders []*BackupProvider `json:"backupprovider"`
+       BackupProviders []*BackupProvider `json:"providers"`
 }
 
 type BackupProvider struct {
diff --git a/cloudstack/CertificateService.go b/cloudstack/CertificateService.go
index 26c5843..8939c35 100644
--- a/cloudstack/CertificateService.go
+++ b/cloudstack/CertificateService.go
@@ -366,8 +366,7 @@ func (s *CertificateService) ListCaCertificate(p 
*ListCaCertificateParams) (*Lis
 }
 
 type ListCaCertificateResponse struct {
-       Count         int              `json:"count"`
-       CaCertificate []*CaCertificate `json:"cacertificate"`
+       CaCertificate *CaCertificate `json:"cacertificates"`
 }
 
 type CaCertificate struct {
diff --git a/cloudstack/ClusterService.go b/cloudstack/ClusterService.go
index 1e10470..e16583d 100644
--- a/cloudstack/ClusterService.go
+++ b/cloudstack/ClusterService.go
@@ -2686,7 +2686,7 @@ func (s *ClusterService) ListClustersMetrics(p 
*ListClustersMetricsParams) (*Lis
 
 type ListClustersMetricsResponse struct {
        Count           int               `json:"count"`
-       ClustersMetrics []*ClustersMetric `json:"clustersmetric"`
+       ClustersMetrics []*ClustersMetric `json:"cluster"`
 }
 
 type ClustersMetric struct {
diff --git a/cloudstack/ExtensionService.go b/cloudstack/ExtensionService.go
index fe3b281..818b4c6 100644
--- a/cloudstack/ExtensionService.go
+++ b/cloudstack/ExtensionService.go
@@ -1197,7 +1197,7 @@ func (s *ExtensionService) ListCustomActions(p 
*ListCustomActionsParams) (*ListC
 
 type ListCustomActionsResponse struct {
        Count         int             `json:"count"`
-       CustomActions []*CustomAction `json:"customaction"`
+       CustomActions []*CustomAction `json:"extensioncustomaction"`
 }
 
 type CustomAction struct {
diff --git a/cloudstack/FirewallService.go b/cloudstack/FirewallService.go
index e6b78c0..d00d50b 100644
--- a/cloudstack/FirewallService.go
+++ b/cloudstack/FirewallService.go
@@ -4094,7 +4094,7 @@ func (s *FirewallService) ListRoutingFirewallRules(p 
*ListRoutingFirewallRulesPa
 
 type ListRoutingFirewallRulesResponse struct {
        Count                int                    `json:"count"`
-       RoutingFirewallRules []*RoutingFirewallRule `json:"routingfirewallrule"`
+       RoutingFirewallRules []*RoutingFirewallRule `json:"firewallrule"`
 }
 
 type RoutingFirewallRule struct {
diff --git a/cloudstack/HostService.go b/cloudstack/HostService.go
index 792b2bc..7706952 100644
--- a/cloudstack/HostService.go
+++ b/cloudstack/HostService.go
@@ -4193,7 +4193,7 @@ func (s *HostService) ListHostsMetrics(p 
*ListHostsMetricsParams) (*ListHostsMet
 
 type ListHostsMetricsResponse struct {
        Count        int            `json:"count"`
-       HostsMetrics []*HostsMetric `json:"hostsmetric"`
+       HostsMetrics []*HostsMetric `json:"host"`
 }
 
 type HostsMetric struct {
diff --git a/cloudstack/NetworkService.go b/cloudstack/NetworkService.go
index 4bf894f..fe99e20 100644
--- a/cloudstack/NetworkService.go
+++ b/cloudstack/NetworkService.go
@@ -3775,7 +3775,7 @@ func (s *NetworkService) ListNetworkIsolationMethods(p 
*ListNetworkIsolationMeth
 
 type ListNetworkIsolationMethodsResponse struct {
        Count                   int                       `json:"count"`
-       NetworkIsolationMethods []*NetworkIsolationMethod 
`json:"networkisolationmethod"`
+       NetworkIsolationMethods []*NetworkIsolationMethod 
`json:"isolationmethod"`
 }
 
 type NetworkIsolationMethod struct {
@@ -6424,7 +6424,7 @@ func (s *NetworkService) ListSupportedNetworkServices(p 
*ListSupportedNetworkSer
 
 type ListSupportedNetworkServicesResponse struct {
        Count                    int                        `json:"count"`
-       SupportedNetworkServices []*SupportedNetworkService 
`json:"supportednetworkservice"`
+       SupportedNetworkServices []*SupportedNetworkService 
`json:"networkservice"`
 }
 
 type SupportedNetworkService struct {
diff --git a/cloudstack/SystemVMService.go b/cloudstack/SystemVMService.go
index c9e573b..2fe912f 100644
--- a/cloudstack/SystemVMService.go
+++ b/cloudstack/SystemVMService.go
@@ -1108,7 +1108,7 @@ func (s *SystemVMService) ListSystemVmsUsageHistory(p 
*ListSystemVmsUsageHistory
 
 type ListSystemVmsUsageHistoryResponse struct {
        Count                 int                      `json:"count"`
-       SystemVmsUsageHistory []*SystemVmsUsageHistory 
`json:"systemvmsusagehistory"`
+       SystemVmsUsageHistory []*SystemVmsUsageHistory `json:"virtualmachine"`
 }
 
 type SystemVmsUsageHistory struct {
diff --git a/cloudstack/UsageService.go b/cloudstack/UsageService.go
index ef89ced..d7310a7 100644
--- a/cloudstack/UsageService.go
+++ b/cloudstack/UsageService.go
@@ -1086,7 +1086,7 @@ func (s *UsageService) ListTrafficTypeImplementors(p 
*ListTrafficTypeImplementor
 
 type ListTrafficTypeImplementorsResponse struct {
        Count                   int                       `json:"count"`
-       TrafficTypeImplementors []*TrafficTypeImplementor 
`json:"traffictypeimplementor"`
+       TrafficTypeImplementors []*TrafficTypeImplementor 
`json:"traffictypeimplementorresponse"`
 }
 
 type TrafficTypeImplementor struct {
@@ -2128,8 +2128,7 @@ func (s *UsageService) ListUsageServerMetrics(p 
*ListUsageServerMetricsParams) (
 }
 
 type ListUsageServerMetricsResponse struct {
-       Count              int                  `json:"count"`
-       UsageServerMetrics []*UsageServerMetric `json:"usageservermetric"`
+       UsageServerMetrics *UsageServerMetric `json:"usageMetrics"`
 }
 
 type UsageServerMetric struct {
diff --git a/cloudstack/UserService.go b/cloudstack/UserService.go
index 10fc217..7c09679 100644
--- a/cloudstack/UserService.go
+++ b/cloudstack/UserService.go
@@ -955,7 +955,7 @@ func (s *UserService) 
ListUserTwoFactorAuthenticatorProviders(p *ListUserTwoFact
 
 type ListUserTwoFactorAuthenticatorProvidersResponse struct {
        Count                               int                                 
  `json:"count"`
-       UserTwoFactorAuthenticatorProviders 
[]*UserTwoFactorAuthenticatorProvider 
`json:"usertwofactorauthenticatorprovider"`
+       UserTwoFactorAuthenticatorProviders 
[]*UserTwoFactorAuthenticatorProvider `json:"providers"`
 }
 
 type UserTwoFactorAuthenticatorProvider struct {
diff --git a/cloudstack/VolumeService.go b/cloudstack/VolumeService.go
index 386c72e..0b7f985 100644
--- a/cloudstack/VolumeService.go
+++ b/cloudstack/VolumeService.go
@@ -4490,7 +4490,7 @@ func (s *VolumeService) ListVolumesMetrics(p 
*ListVolumesMetricsParams) (*ListVo
 
 type ListVolumesMetricsResponse struct {
        Count          int              `json:"count"`
-       VolumesMetrics []*VolumesMetric `json:"volumesmetric"`
+       VolumesMetrics []*VolumesMetric `json:"volume"`
 }
 
 type VolumesMetric struct {
diff --git a/cloudstack/ZoneService.go b/cloudstack/ZoneService.go
index a9f0329..40440f7 100644
--- a/cloudstack/ZoneService.go
+++ b/cloudstack/ZoneService.go
@@ -2113,7 +2113,7 @@ func (s *ZoneService) ListIpv4SubnetsForZone(p 
*ListIpv4SubnetsForZoneParams) (*
 
 type ListIpv4SubnetsForZoneResponse struct {
        Count              int                   `json:"count"`
-       Ipv4SubnetsForZone []*Ipv4SubnetsForZone `json:"ipv4subnetsforzone"`
+       Ipv4SubnetsForZone []*Ipv4SubnetsForZone `json:"zoneipv4subnet"`
 }
 
 type Ipv4SubnetsForZone struct {
@@ -3076,7 +3076,7 @@ func (s *ZoneService) ListZonesMetrics(p 
*ListZonesMetricsParams) (*ListZonesMet
 
 type ListZonesMetricsResponse struct {
        Count        int            `json:"count"`
-       ZonesMetrics []*ZonesMetric `json:"zonesmetric"`
+       ZonesMetrics []*ZonesMetric `json:"zone"`
 }
 
 type ZonesMetric struct {
diff --git a/generate/generate.go b/generate/generate.go
index d1d0802..cc73c7c 100644
--- a/generate/generate.go
+++ b/generate/generate.go
@@ -179,6 +179,85 @@ var customResponseStructTypes = map[string]string{
        "findHostsForMigration": "HostForMigration",
 }
 
+// listResponseKeys records the JSON key CloudStack uses for the items of a 
list
+// response, for every API where that key differs from the one derived from the
+// API name.
+//
+// The key cannot be computed. CloudStack keys the item array by the *response
+// object* name (UserVmResponse serialises as "virtualmachine", 
TemplateResponse
+// as "template"), while the fallback below derives it from the API name. When 
an
+// API's response object does not share its name, the two disagree and there is
+// nothing in listApis to reconcile them from: it reports each response's 
fields
+// but never the object name, and `related` is empty for exactly these APIs.
+//
+// Getting this wrong fails silently and is therefore expensive: `count` parses
+// from the same body, so a caller sees a correct count beside an empty slice 
and
+// no error. Do not add a guessed entry; an entry invented from the API name is
+// as harmful as a missing one, because it looks deliberate. Every key here was
+// observed in the response of a real management server, and
+// test/ListResponseJSONTagsRegression_test.go pins them so a regeneration
+// cannot quietly undo one.
+var listResponseKeys = map[string]string{
+       "listAsyncJobs":                   "asyncjobs",
+       "listDomainChildren":              "domain",
+       "listEgressFirewallRules":         "firewallrule",
+       "listGuestNetworkIpv6Prefixes":    "guestnetworkipv6prefix",
+       "listHostHAProviders":             "haprovider",
+       "listHostHAResources":             "hostha",
+       "listHypervisorCapabilities":      "hypervisorCapabilities",
+       "listImageStoreObjects":           "datastoreobject",
+       "listLBHealthCheckPolicies":       "healthcheckpolicies",
+       "listLBStickinessPolicies":        "stickinesspolicies",
+       "listManagementServersMetrics":    "managementserver",
+       "listObjectStoragePools":          "objectstore",
+       "listSecondaryStorageSelectors":   "heuristics",
+       "listStoragePoolObjects":          "datastoreobject",
+       "listStoragePoolsMetrics":         "storagepool",
+       "listVirtualMachinesMetrics":      "virtualmachine",
+       "listVirtualMachinesUsageHistory": "virtualmachine",
+       "listVmwareDcVms":                 "unmanagedinstance",
+       "listVolumesUsageHistory":         "volume",
+       "quotaSummary":                    "summary",
+       "quotaTariffList":                 "quotatariff",
+       "registerTemplate":                "template",
+
+       // ListVnfTemplatesCmd is an empty subclass of ListTemplatesCmd and
+       // ListVnfAppliancesCmd inherits execute() from ListVMsCmd, so both 
return
+       // the response object of the command they extend.
+       "listVnfAppliances": "virtualmachine",
+       "listVnfTemplates":  "template",
+
+       // Found by sweeping every list API against two 4.22.1.0 servers and 
reading
+       // the raw response keys. Each had been decoding as a permanently empty 
slice.
+       "listBackupProviders":                     "providers",
+       "listClustersMetrics":                     "cluster",
+       "listCustomActions":                       "extensioncustomaction",
+       "listHostsMetrics":                        "host",
+       "listNetworkIsolationMethods":             "isolationmethod",
+       "listRoutingFirewallRules":                "firewallrule",
+       "listSupportedNetworkServices":            "networkservice",
+       "listSystemVmsUsageHistory":               "virtualmachine",
+       "listTrafficTypeImplementors":             
"traffictypeimplementorresponse",
+       "listUserTwoFactorAuthenticatorProviders": "providers",
+       "listVolumesMetrics":                      "volume",
+       "listZonesMetrics":                        "zone",
+
+       // Found by populating a simulator so these two returned data at all. 
Both
+       // are corroborated by setObjectName in the CloudStack source and by 
the keys
+       // the CloudStack UI itself reads.
+       "listASNRanges":          "asnumberrange",
+       "listIpv4SubnetsForZone": "zoneipv4subnet",
+}
+
+// listResponseKey returns the JSON key for an API's list items, preferring an
+// observed key over the one derived from the API name.
+func listResponseKey(apiName, listName string) string {
+       if key, ok := listResponseKeys[apiName]; ok {
+               return key
+       }
+       return strings.ToLower(parseSingular(listName))
+}
+
 // We prefill this one value to make sure it is not
 // created twice, as this is also a top level type.
 var typeNames = map[string]bool{"Nic": true}
@@ -726,6 +805,12 @@ func (as *allServices) GeneralCode() ([]byte, error) {
        pn("                            if err := json.Unmarshal(v, &resp); err 
!= nil {")
        pn("                                    return nil, err")
        pn("                            }")
+       // Guard added in #154. It has to live here rather than in the generated
+       // file, otherwise `make code` reverts it, which is exactly what 
happened
+       // between #154 and now.
+       pn("                            if len(resp) == 0 {")
+       pn("                                    return nil, fmt.Errorf(\"Unable 
to extract raw value: empty array for key %%q in:\\n\\n%%s\\n\\n\", k, 
string(b))")
+       pn("                            }")
        pn("                            return resp[0], nil")
        pn("                    }")
        pn("            }")
@@ -2078,40 +2163,28 @@ func (s *service) generateResponseType(a *API) {
                a.Name == "registerCniConfiguration" || a.Name == 
"quotaBalance" || a.Name == "quotaSummary" || a.Name == "quotaTariffList" {
                pn("type %s struct {", tn)
 
-               // This nasty check is for some specific response that do not 
behave consistent
+               // Responses whose *shape* differs: a single object instead of 
an array,
+               // no count, more than one collection, or a hand-written field 
list.
+               // Responses that differ only in the item key are handled by 
the default
+               // arm through listResponseKeys.
                switch a.Name {
-               case "listAsyncJobs":
-                       pn("    Count int `json:\"count\"`")
-                       pn("    %s []*%s `json:\"%s\"`", ln, parseSingular(ln), 
"asyncjobs")
                case "listCapabilities":
                        pn("    %s *%s `json:\"%s\"`", ln, parseSingular(ln), 
"capability")
-               case "listEgressFirewallRules":
-                       pn("    Count int `json:\"count\"`")
-                       pn("    %s []*%s `json:\"%s\"`", ln, parseSingular(ln), 
"firewallrule")
+               case "listCaCertificate":
+                       // Returns a single object under "cacertificates", with 
no count.
+                       pn("    %s *%s `json:\"%s\"`", ln, parseSingular(ln), 
"cacertificates")
+               case "listUsageServerMetrics":
+                       // Returns a single object under "usageMetrics", with 
no count.
+                       pn("    %s *%s `json:\"%s\"`", ln, parseSingular(ln), 
"usageMetrics")
                case "listLoadBalancerRuleInstances":
                        pn("    Count int `json:\"count\"`")
                        pn("    LBRuleVMIDIPs []*%s `json:\"%s\"`", 
parseSingular(ln), "lbrulevmidip")
                        pn("    LoadBalancerRuleInstances []*VirtualMachine 
`json:\"%s\"`", strings.ToLower(parseSingular(ln)))
-               case "listVirtualMachinesMetrics":
-                       pn("    Count int `json:\"count\"`")
-                       pn("    %s []*%s `json:\"%s\"`", ln, parseSingular(ln), 
"virtualmachine")
-               case "listManagementServersMetrics":
-                       pn("    Count int `json:\"count\"`")
-                       pn("    %s []*%s `json:\"%s\"`", ln, parseSingular(ln), 
"managementserver")
                case "listDbMetrics":
                        pn("    %s %s `json:\"%s\"`", ln, parseSingular(ln), 
"dbMetrics")
-               case "registerTemplate":
-                       pn("    Count int `json:\"count\"`")
-                       pn("    %s []*%s `json:\"%s\"`", ln, parseSingular(ln), 
"template")
-               case "listDomainChildren":
-                       pn("    Count int `json:\"count\"`")
-                       pn("    %s []*%s `json:\"%s\"`", ln, parseSingular(ln), 
"domain")
                case "findHostsForMigration":
                        pn(" Count int `json:\"count\"`")
                        pn(" Host []*%s `json:\"%s\"`", 
customResponseStructTypes[a.Name], "host")
-               case "listVmwareDcVms":
-                       pn("    Count int `json:\"count\"`")
-                       pn("    %s []*%s `json:\"%s\"`", ln, parseSingular(ln), 
"unmanagedinstance")
                case "registerUserData":
                        pn("    Account string `json:\"account\"`")
                        pn("    Accountid string `json:\"accountid\"`")
@@ -2126,69 +2199,14 @@ func (s *service) generateResponseType(a *API) {
                        pn("    Userdata string `json:\"userdata\"`")
                case "registerCniConfiguration":
                        pn("    CniConfiguration *UserData 
`json:\"cniconfig\"`")
-               case "listObjectStoragePools":
-                       pn("    Count int `json:\"count\"`")
-                       pn("    %s []*%s `json:\"%s\"`", ln, parseSingular(ln), 
"objectstore")
-               case "listStoragePoolObjects":
-                       pn("    Count int `json:\"count\"`")
-                       pn("    %s []*%s `json:\"%s\"`", ln, parseSingular(ln), 
"datastoreobject")
-               case "listImageStoreObjects":
-                       pn("    Count int `json:\"count\"`")
-                       pn("    %s []*%s `json:\"%s\"`", ln, parseSingular(ln), 
"datastoreobject")
-               case "listVolumesUsageHistory":
-                       pn("    Count int `json:\"count\"`")
-                       pn("    %s []*%s `json:\"%s\"`", ln, parseSingular(ln), 
"volume")
-               case "listHostHAProviders":
-                       pn("    Count int `json:\"count\"`")
-                       pn("    %s []*%s `json:\"%s\"`", ln, parseSingular(ln), 
"haprovider")
-               case "listSecondaryStorageSelectors":
-                       pn("    Count int `json:\"count\"`")
-                       pn("    %s []*%s `json:\"%s\"`", ln, parseSingular(ln), 
"heuristics")
-               case "listVirtualMachinesUsageHistory":
-                       pn("    Count                       int                 
           `json:\"count\"`")
-                       pn("    VirtualMachinesUsageHistory 
[]*VirtualMachinesUsageHistory `json:\"virtualmachine\"`")
-               case "listHostHAResources":
-                       pn("    Count int `json:\"count\"`")
-                       pn("    %s []*%s `json:\"%s\"`", ln, parseSingular(ln), 
"hostha")
                case "listInfrastructure":
                        pn("    Count int `json:\"count\"`")
                        pn("    %s *%s `json:\"%s\"`", ln, parseSingular(ln), 
"infrastructure")
-               case "listStoragePoolsMetrics":
-                       pn("    Count int `json:\"count\"`")
-                       pn("    %s []*%s `json:\"%s\"`", ln, parseSingular(ln), 
"storagepool")
-               case "quotaTariffList":
-                       pn("    Count int `json:\"count\"`")
-                       pn("    %s []*%s `json:\"%s\"`", ln, parseSingular(ln), 
"quotatariff")
                case "quotaBalance":
                        pn("    %s []*%s `json:\"%s\"`", ln, parseSingular(ln), 
"balance")
-               case "quotaSummary":
-                       pn("    Count int `json:\"count\"`")
-                       pn("    %s []*%s `json:\"%s\"`", ln, parseSingular(ln), 
"summary")
-               case "listHypervisorCapabilities":
-                       pn("    Count int `json:\"count\"`")
-                       pn("    %s []*%s `json:\"%s\"`", ln, parseSingular(ln), 
"hypervisorCapabilities")
-               case "listGuestNetworkIpv6Prefixes":
-                       pn("    Count int `json:\"count\"`")
-                       pn("    %s []*%s `json:\"%s\"`", ln, parseSingular(ln), 
"guestnetworkipv6prefix")
-               case "listLBHealthCheckPolicies":
-                       pn("    Count int `json:\"count\"`")
-                       pn("    %s []*%s `json:\"%s\"`", ln, parseSingular(ln), 
"healthcheckpolicies")
-               case "listLBStickinessPolicies":
-                       pn("    Count int `json:\"count\"`")
-                       pn("    %s []*%s `json:\"%s\"`", ln, parseSingular(ln), 
"stickinesspolicies")
-               case "listVnfTemplates":
-                       // ListVnfTemplatesCmd is an empty subclass of 
ListTemplatesCmd, so the
-                       // server returns the items under "template", not 
"vnftemplate".
-                       pn("    Count int `json:\"count\"`")
-                       pn("    %s []*%s `json:\"%s\"`", ln, parseSingular(ln), 
"template")
-               case "listVnfAppliances":
-                       // ListVnfAppliancesCmd inherits execute() from 
ListVMsCmd, so the server
-                       // returns the items under "virtualmachine", not 
"vnfappliance".
-                       pn("    Count int `json:\"count\"`")
-                       pn("    %s []*%s `json:\"%s\"`", ln, parseSingular(ln), 
"virtualmachine")
                default:
                        pn("    Count int `json:\"count\"`")
-                       pn("    %s []*%s `json:\"%s\"`", ln, parseSingular(ln), 
strings.ToLower(parseSingular(ln)))
+                       pn("    %s []*%s `json:\"%s\"`", ln, parseSingular(ln), 
listResponseKey(a.Name, ln))
                }
                pn("}")
                pn("")
diff --git a/test/ListResponseJSONTagsRegression_test.go 
b/test/ListResponseJSONTagsRegression_test.go
index 1552328..9bea38a 100644
--- a/test/ListResponseJSONTagsRegression_test.go
+++ b/test/ListResponseJSONTagsRegression_test.go
@@ -71,3 +71,199 @@ func TestListResponseJSONTagsPopulateSlices(t *testing.T) {
                }
        })
 }
+
+// Regression test for the response keys found by sweeping every list API 
against
+// live 4.22.1.0 servers and reading the raw response keys. Each of these 
decoded
+// as a permanently empty slice before the keys were corrected in
+// generate/generate.go, and each failed silently: count parses from the same
+// body, so callers saw a correct count beside no results and no error.
+//
+// These assertions exist because the key cannot be derived from the API name, 
so
+// a regeneration can otherwise quietly reintroduce any of them.
+func TestListResponseKeysObservedFromServer(t *testing.T) {
+       sliceCases := []struct {
+               name   string
+               key    string
+               body   string
+               decode func([]byte) (int, error)
+       }{
+               {
+                       name: "ASNRanges", key: "asnumberrange",
+                       body: `{"count":1,"asnumberrange":[{}]}`,
+                       decode: func(b []byte) (int, error) {
+                               var r cloudstack.ListASNRangesResponse
+                               err := json.Unmarshal(b, &r)
+                               return len(r.ASNRanges), err
+                       },
+               },
+               {
+                       name: "Ipv4SubnetsForZone", key: "zoneipv4subnet",
+                       body: `{"count":1,"zoneipv4subnet":[{}]}`,
+                       decode: func(b []byte) (int, error) {
+                               var r cloudstack.ListIpv4SubnetsForZoneResponse
+                               err := json.Unmarshal(b, &r)
+                               return len(r.Ipv4SubnetsForZone), err
+                       },
+               },
+               {
+                       name: "BackupProviders", key: "providers",
+                       body: `{"count":1,"providers":[{}]}`,
+                       decode: func(b []byte) (int, error) {
+                               var r cloudstack.ListBackupProvidersResponse
+                               err := json.Unmarshal(b, &r)
+                               return len(r.BackupProviders), err
+                       },
+               },
+               {
+                       name: "ClustersMetrics", key: "cluster",
+                       body: `{"count":1,"cluster":[{}]}`,
+                       decode: func(b []byte) (int, error) {
+                               var r cloudstack.ListClustersMetricsResponse
+                               err := json.Unmarshal(b, &r)
+                               return len(r.ClustersMetrics), err
+                       },
+               },
+               {
+                       name: "CustomActions", key: "extensioncustomaction",
+                       body: `{"count":1,"extensioncustomaction":[{}]}`,
+                       decode: func(b []byte) (int, error) {
+                               var r cloudstack.ListCustomActionsResponse
+                               err := json.Unmarshal(b, &r)
+                               return len(r.CustomActions), err
+                       },
+               },
+               {
+                       name: "HostsMetrics", key: "host",
+                       body: `{"count":1,"host":[{}]}`,
+                       decode: func(b []byte) (int, error) {
+                               var r cloudstack.ListHostsMetricsResponse
+                               err := json.Unmarshal(b, &r)
+                               return len(r.HostsMetrics), err
+                       },
+               },
+               {
+                       name: "NetworkIsolationMethods", key: "isolationmethod",
+                       body: `{"count":1,"isolationmethod":[{}]}`,
+                       decode: func(b []byte) (int, error) {
+                               var r 
cloudstack.ListNetworkIsolationMethodsResponse
+                               err := json.Unmarshal(b, &r)
+                               return len(r.NetworkIsolationMethods), err
+                       },
+               },
+               {
+                       name: "RoutingFirewallRules", key: "firewallrule",
+                       body: `{"count":1,"firewallrule":[{}]}`,
+                       decode: func(b []byte) (int, error) {
+                               var r 
cloudstack.ListRoutingFirewallRulesResponse
+                               err := json.Unmarshal(b, &r)
+                               return len(r.RoutingFirewallRules), err
+                       },
+               },
+               {
+                       name: "SupportedNetworkServices", key: "networkservice",
+                       body: `{"count":1,"networkservice":[{}]}`,
+                       decode: func(b []byte) (int, error) {
+                               var r 
cloudstack.ListSupportedNetworkServicesResponse
+                               err := json.Unmarshal(b, &r)
+                               return len(r.SupportedNetworkServices), err
+                       },
+               },
+               {
+                       name: "SystemVmsUsageHistory", key: "virtualmachine",
+                       body: `{"count":1,"virtualmachine":[{}]}`,
+                       decode: func(b []byte) (int, error) {
+                               var r 
cloudstack.ListSystemVmsUsageHistoryResponse
+                               err := json.Unmarshal(b, &r)
+                               return len(r.SystemVmsUsageHistory), err
+                       },
+               },
+               {
+                       name: "TrafficTypeImplementors", key: 
"traffictypeimplementorresponse",
+                       body: 
`{"count":1,"traffictypeimplementorresponse":[{}]}`,
+                       decode: func(b []byte) (int, error) {
+                               var r 
cloudstack.ListTrafficTypeImplementorsResponse
+                               err := json.Unmarshal(b, &r)
+                               return len(r.TrafficTypeImplementors), err
+                       },
+               },
+               {
+                       name: "UserTwoFactorAuthenticatorProviders", key: 
"providers",
+                       body: `{"count":1,"providers":[{}]}`,
+                       decode: func(b []byte) (int, error) {
+                               var r 
cloudstack.ListUserTwoFactorAuthenticatorProvidersResponse
+                               err := json.Unmarshal(b, &r)
+                               return 
len(r.UserTwoFactorAuthenticatorProviders), err
+                       },
+               },
+               {
+                       name: "VolumesMetrics", key: "volume",
+                       body: `{"count":1,"volume":[{}]}`,
+                       decode: func(b []byte) (int, error) {
+                               var r cloudstack.ListVolumesMetricsResponse
+                               err := json.Unmarshal(b, &r)
+                               return len(r.VolumesMetrics), err
+                       },
+               },
+               {
+                       name: "ZonesMetrics", key: "zone",
+                       body: `{"count":1,"zone":[{}]}`,
+                       decode: func(b []byte) (int, error) {
+                               var r cloudstack.ListZonesMetricsResponse
+                               err := json.Unmarshal(b, &r)
+                               return len(r.ZonesMetrics), err
+                       },
+               },
+       }
+
+       for _, tc := range sliceCases {
+               t.Run(tc.name, func(t *testing.T) {
+                       n, err := tc.decode([]byte(tc.body))
+                       if err != nil {
+                               t.Fatalf("decoding key %q: %v", tc.key, err)
+                       }
+                       if n != 1 {
+                               t.Fatalf("expected 1 item under key %q, got %d 
(nil slice means the json tag does not match the key the server sends)", 
tc.key, n)
+                       }
+               })
+       }
+
+       // These two return a single object and no count, so the field is a 
pointer
+       // rather than a slice.
+       objectCases := []struct {
+               name   string
+               key    string
+               body   string
+               decode func([]byte) (bool, error)
+       }{
+               {
+                       name: "CaCertificate", key: "cacertificates",
+                       body: `{"cacertificates":{}}`,
+                       decode: func(b []byte) (bool, error) {
+                               var r cloudstack.ListCaCertificateResponse
+                               err := json.Unmarshal(b, &r)
+                               return r.CaCertificate != nil, err
+                       },
+               },
+               {
+                       name: "UsageServerMetrics", key: "usageMetrics",
+                       body: `{"usageMetrics":{}}`,
+                       decode: func(b []byte) (bool, error) {
+                               var r cloudstack.ListUsageServerMetricsResponse
+                               err := json.Unmarshal(b, &r)
+                               return r.UsageServerMetrics != nil, err
+                       },
+               },
+       }
+
+       for _, tc := range objectCases {
+               t.Run(tc.name, func(t *testing.T) {
+                       ok, err := tc.decode([]byte(tc.body))
+                       if err != nil {
+                               t.Fatalf("decoding key %q: %v", tc.key, err)
+                       }
+                       if !ok {
+                               t.Fatalf("expected an object under key %q, got 
nil", tc.key)
+                       }
+               })
+       }
+}

Reply via email to