You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by ab...@apache.org on 2026/08/11 12:10:11 UTC

(cloudstack-go) 01/01: Fix list response keys derived from the API name

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 <63...@users.noreply.github.com>
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)
+			}
+		})
+	}
+}