Repository: hetznercloud/hcloud-go
Pull request #545 (2024-11-04 14:16:16)

feat: use custom IDOrName type for schemas
Add typing information to the fields that support passing either an ID as int, an ID as string or a Name as string. An empty value will be marshalled to `null` to reflect the behavior of an empty interface.

After this change, we do not have to cast IDs to float64 or to string to be able to read the values.  

Since this is part of the schema package, this is not a breaking change for our customers, but might break our tests that rely on casting the interface to the desired type.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/hetznercloud/hcloud-go/pull/545?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud) Report
Attention: Patch coverage is `75.00000%` with `14 lines` in your changes missing coverage. Please review.
> Project coverage is 67.72%. Comparing base [(`9903f4d`)](https://app.codecov.io/gh/hetznercloud/hcloud-go/commit/9903f4d34307ac8b3272a2a862cc41706db7b3a0?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud) to head [(`36f7d23`)](https://app.codecov.io/gh/hetznercloud/hcloud-go/commit/36f7d23b552704a85eb2a071f248480b67fd81e3?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud).

| [Files with missing lines](https://app.codecov.io/gh/hetznercloud/hcloud-go/pull/545?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud) | Patch % | Lines |
|---|---|---|
| [hcloud/schema/id\_or\_name.go](https://app.codecov.io/gh/hetznercloud/hcloud-go/pull/545?src=pr&el=tree&filepath=hcloud%2Fschema%2Fid_or_name.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud#diff-aGNsb3VkL3NjaGVtYS9pZF9vcl9uYW1lLmdv) | 66.66% | [12 Missing and 2 partials :warning: ](https://app.codecov.io/gh/hetznercloud/hcloud-go/pull/545?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud) |

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main     #545   +/-   ##
=======================================
  Coverage   67.71%   67.72%           
=======================================
  Files          46       47    +1     
  Lines        4882     4902   +20     
=======================================
+ Hits         3306     3320   +14     
- Misses       1166     1173    +7     
+ Partials      410      409    -1     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/hetznercloud/hcloud-go/pull/545?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud).


Patch:
diff --git a/hcloud/load_balancer.go b/hcloud/load_balancer.go
index fb40057..c634d16 100644
--- a/hcloud/load_balancer.go
+++ b/hcloud/load_balancer.go
@@ -935,10 +935,8 @@ type LoadBalancerChangeTypeOpts struct {
 // ChangeType changes a Load Balancer's type.
 func (c *LoadBalancerClient) ChangeType(ctx context.Context, loadBalancer *LoadBalancer, opts LoadBalancerChangeTypeOpts) (*Action, *Response, error) {
 	reqBody := schema.LoadBalancerActionChangeTypeRequest{}
-	if opts.LoadBalancerType.ID != 0 {
-		reqBody.LoadBalancerType = opts.LoadBalancerType.ID
-	} else {
-		reqBody.LoadBalancerType = opts.LoadBalancerType.Name
+	if opts.LoadBalancerType.ID != 0 || opts.LoadBalancerType.Name != "" {
+		reqBody.LoadBalancerType = schema.IDOrName{ID: opts.LoadBalancerType.ID, Name: opts.LoadBalancerType.Name}
 	}
 	reqBodyData, err := json.Marshal(reqBody)
 	if err != nil {
diff --git a/hcloud/schema/id_or_name.go b/hcloud/schema/id_or_name.go
new file mode 100644
index 0000000..75e1169
--- /dev/null
+++ b/hcloud/schema/id_or_name.go
@@ -0,0 +1,68 @@
+package schema
+
+import (
+	"bytes"
+	"encoding/json"
+	"reflect"
+	"strconv"
+)
+
+// IDOrName can be used in API requests where either a resource id or name can be
+// specified.
+type IDOrName struct {
+	ID   int64
+	Name string
+}
+
+var _ json.Unmarshaler = (*IDOrName)(nil)
+var _ json.Marshaler = (*IDOrName)(nil)
+
+func (o IDOrName) MarshalJSON() ([]byte, error) {
+	if o.ID != 0 {
+		return json.Marshal(o.ID)
+	}
+	if o.Name != "" {
+		return json.Marshal(o.Name)
+	}
+
+	// We want to preserve the behavior of an empty interface{} to prevent breaking
+	// changes (marshaled to null when empty).
+	return json.Marshal(nil)
+}
+
+func (o *IDOrName) UnmarshalJSON(data []byte) error {
+	d := json.NewDecoder(bytes.NewBuffer(data))
+	// This ensures we won't lose precision on large IDs, see json.Number below
+	d.UseNumber()
+
+	var v any
+	if err := d.Decode(&v); err != nil {
+		return err
+	}
+
+	switch typed := v.(type) {
+	case string:
+		id, err := strconv.ParseInt(typed, 10, 64)
+		if err == nil {
+			o.ID = id
+		} else if typed != "" {
+			o.Name = typed
+		}
+	case json.Number:
+		id, err := typed.Int64()
+		if err != nil {
+			return &json.UnmarshalTypeError{
+				Value: string(data),
+				Type:  reflect.TypeOf(*o),
+			}
+		}
+		o.ID = id
+	default:
+		return &json.UnmarshalTypeError{
+			Value: string(data),
+			Type:  reflect.TypeOf(*o),
+		}
+	}
+
+	return nil
+}
diff --git a/hcloud/schema/load_balancer.go b/hcloud/schema/load_balancer.go
index 7e1c4f5..d8760ad 100644
--- a/hcloud/schema/load_balancer.go
+++ b/hcloud/schema/load_balancer.go
@@ -251,7 +251,7 @@ type LoadBalancerDeleteServiceResponse struct {
 
 type LoadBalancerCreateRequest struct {
 	Name             string                              `json:"name"`
-	LoadBalancerType interface{}                         `json:"load_balancer_type"` // int or string
+	LoadBalancerType IDOrName                            `json:"load_balancer_type"`
 	Algorithm        *LoadBalancerCreateRequestAlgorithm `json:"algorithm,omitempty"`
 	Location         *string                             `json:"location,omitempty"`
 	NetworkZone      *string                             `json:"network_zone,omitempty"`
@@ -380,7 +380,7 @@ type LoadBalancerActionDisablePublicInterfaceResponse struct {
 }
 
 type LoadBalancerActionChangeTypeRequest struct {
-	LoadBalancerType interface{} `json:"load_balancer_type"` // int or string
+	LoadBalancerType IDOrName `json:"load_balancer_type"`
 }
 
 type LoadBalancerActionChangeTypeResponse struct {
diff --git a/hcloud/schema/server.go b/hcloud/schema/server.go
index b9c945a..72aaf26 100644
--- a/hcloud/schema/server.go
+++ b/hcloud/schema/server.go
@@ -99,8 +99,8 @@ type ServerListResponse struct {
 // create a server.
 type ServerCreateRequest struct {
 	Name             string                  `json:"name"`
-	ServerType       interface{}             `json:"server_type"` // int or string
-	Image            interface{}             `json:"image"`       // int or string
+	ServerType       IDOrName                `json:"server_type"`
+	Image            IDOrName                `json:"image"`
 	SSHKeys          []int64                 `json:"ssh_keys,omitempty"`
 	Location         string                  `json:"location,omitempty"`
 	Datacenter       string                  `json:"datacenter,omitempty"`
@@ -257,7 +257,7 @@ type ServerActionDisableRescueResponse struct {
 // ServerActionRebuildRequest defines the schema for the request to
 // rebuild a server.
 type ServerActionRebuildRequest struct {
-	Image interface{} `json:"image"` // int or string
+	Image IDOrName `json:"image"`
 }
 
 // ServerActionRebuildResponse defines the schema of the response when
@@ -270,7 +270,7 @@ type ServerActionRebuildResponse struct {
 // ServerActionAttachISORequest defines the schema for the request to
 // attach an ISO to a server.
 type ServerActionAttachISORequest struct {
-	ISO interface{} `json:"iso"` // int or string
+	ISO IDOrName `json:"iso"`
 }
 
 // ServerActionAttachISOResponse defines the schema of the response when
@@ -308,8 +308,8 @@ type ServerActionDisableBackupResponse struct {
 // ServerActionChangeTypeRequest defines the schema for the request to
 // change a server's type.
 type ServerActionChangeTypeRequest struct {
-	ServerType  interface{} `json:"server_type"` // int or string
-	UpgradeDisk bool        `json:"upgrade_disk"`
+	ServerType  IDOrName `json:"server_type"`
+	UpgradeDisk bool     `json:"upgrade_disk"`
 }
 
 // ServerActionChangeTypeResponse defines the schema of the response when
diff --git a/hcloud/schema/volume.go b/hcloud/schema/volume.go
index 1de6459..89223ba 100644
--- a/hcloud/schema/volume.go
+++ b/hcloud/schema/volume.go
@@ -23,7 +23,7 @@ type VolumeCreateRequest struct {
 	Name      string             `json:"name"`
 	Size      int                `json:"size"`
 	Server    *int64             `json:"server,omitempty"`
-	Location  interface{}        `json:"location,omitempty"` // int, string, or nil
+	Location  *IDOrName          `json:"location,omitempty"`
 	Labels    *map[string]string `json:"labels,omitempty"`
 	Automount *bool              `json:"automount,omitempty"`
 	Format    *string            `json:"format,omitempty"`
diff --git a/hcloud/schema_gen.go b/hcloud/schema_gen.go
index c2c3735..4566acf 100644
--- a/hcloud/schema_gen.go
+++ b/hcloud/schema_gen.go
@@ -69,7 +69,6 @@ You can find a documentation of goverter here: https://goverter.jmattheis.de/
 // goverter:extend durationFromIntSeconds
 // goverter:extend intSecondsFromDuration
 // goverter:extend serverFromImageCreatedFromSchema
-// goverter:extend anyFromLoadBalancerType
 // goverter:extend serverMetricsTimeSeriesFromSchema
 // goverter:extend loadBalancerMetricsTimeSeriesFromSchema
 // goverter:extend stringPtrFromLoadBalancerServiceProtocol
@@ -808,16 +807,6 @@ func volumePricingFromSchema(s schema.Pricing) VolumePricing {
 	}
 }
 
-func anyFromLoadBalancerType(t *LoadBalancerType) interface{} {
-	if t == nil {
-		return nil
-	}
-	if t.ID != 0 {
-		return t.ID
-	}
-	return t.Name
-}
-
 func serverMetricsTimeSeriesFromSchema(s schema.ServerTimeSeriesVals) ([]ServerMetricsValue, error) {
 	vals := make([]ServerMetricsValue, len(s.Values))
 
diff --git a/hcloud/server.go b/hcloud/server.go
index 11857e1..d1514f5 100644
--- a/hcloud/server.go
+++ b/hcloud/server.go
@@ -378,15 +378,11 @@ func (c *ServerClient) Create(ctx context.Context, opts ServerCreateOpts) (Serve
 	reqBody.Name = opts.Name
 	reqBody.Automount = opts.Automount
 	reqBody.StartAfterCreate = opts.StartAfterCreate
-	if opts.ServerType.ID != 0 {
-		reqBody.ServerType = opts.ServerType.ID
-	} else if opts.ServerType.Name != "" {
-		reqBody.ServerType = opts.ServerType.Name
+	if opts.ServerType.ID != 0 || opts.ServerType.Name != "" {
+		reqBody.ServerType = schema.IDOrName{ID: opts.ServerType.ID, Name: opts.ServerType.Name}
 	}
-	if opts.Image.ID != 0 {
-		reqBody.Image = opts.Image.ID
-	} else if opts.Image.Name != "" {
-		reqBody.Image = opts.Image.Name
+	if opts.Image.ID != 0 || opts.Image.Name != "" {
+		reqBody.Image = schema.IDOrName{ID: opts.Image.ID, Name: opts.Image.Name}
 	}
 	if opts.Labels != nil {
 		reqBody.Labels = &opts.Labels
@@ -778,10 +774,8 @@ func (c *ServerClient) Rebuild(ctx context.Context, server *Server, opts ServerR
 // RebuildWithResult rebuilds a server.
 func (c *ServerClient) RebuildWithResult(ctx context.Context, server *Server, opts ServerRebuildOpts) (ServerRebuildResult, *Response, error) {
 	reqBody := schema.ServerActionRebuildRequest{}
-	if opts.Image.ID != 0 {
-		reqBody.Image = opts.Image.ID
-	} else {
-		reqBody.Image = opts.Image.Name
+	if opts.Image.ID != 0 || opts.Image.Name != "" {
+		reqBody.Image = schema.IDOrName{ID: opts.Image.ID, Name: opts.Image.Name}
 	}
 	reqBodyData, err := json.Marshal(reqBody)
 	if err != nil {
@@ -813,10 +807,8 @@ func (c *ServerClient) RebuildWithResult(ctx context.Context, server *Server, op
 // AttachISO attaches an ISO to a server.
 func (c *ServerClient) AttachISO(ctx context.Context, server *Server, iso *ISO) (*Action, *Response, error) {
 	reqBody := schema.ServerActionAttachISORequest{}
-	if iso.ID != 0 {
-		reqBody.ISO = iso.ID
-	} else {
-		reqBody.ISO = iso.Name
+	if iso.ID != 0 || iso.Name != "" {
+		reqBody.ISO = schema.IDOrName{ID: iso.ID, Name: iso.Name}
 	}
 	reqBodyData, err := json.Marshal(reqBody)
 	if err != nil {
@@ -899,10 +891,8 @@ func (c *ServerClient) ChangeType(ctx context.Context, server *Server, opts Serv
 	reqBody := schema.ServerActionChangeTypeRequest{
 		UpgradeDisk: opts.UpgradeDisk,
 	}
-	if opts.ServerType.ID != 0 {
-		reqBody.ServerType = opts.ServerType.ID
-	} else {
-		reqBody.ServerType = opts.ServerType.Name
+	if opts.ServerType.ID != 0 || opts.ServerType.Name != "" {
+		reqBody.ServerType = schema.IDOrName{ID: opts.ServerType.ID, Name: opts.ServerType.Name}
 	}
 	reqBodyData, err := json.Marshal(reqBody)
 	if err != nil {
diff --git a/hcloud/volume.go b/hcloud/volume.go
index c744b5a..6fe9d5a 100644
--- a/hcloud/volume.go
+++ b/hcloud/volume.go
@@ -220,10 +220,8 @@ func (c *VolumeClient) Create(ctx context.Context, opts VolumeCreateOpts) (Volum
 		reqBody.Server = Ptr(opts.Server.ID)
 	}
 	if opts.Location != nil {
-		if opts.Location.ID != 0 {
-			reqBody.Location = opts.Location.ID
-		} else {
-			reqBody.Location = opts.Location.Name
+		if opts.Location.ID != 0 || opts.Location.Name != "" {
+			reqBody.Location = &schema.IDOrName{ID: opts.Location.ID, Name: opts.Location.Name}
 		}
 	}
 
diff --git a/hcloud/zz_schema.go b/hcloud/zz_schema.go
old mode 100755
new mode 100644
index c739e1d..4f4b866
--- a/hcloud/zz_schema.go
+++ b/hcloud/zz_schema.go
@@ -690,7 +690,7 @@ func (c *converterImpl) SchemaFromLoadBalancerAddServiceOpts(source LoadBalancer
 func (c *converterImpl) SchemaFromLoadBalancerCreateOpts(source LoadBalancerCreateOpts) schema.LoadBalancerCreateRequest {
 	var schemaLoadBalancerCreateRequest schema.LoadBalancerCreateRequest
 	schemaLoadBalancerCreateRequest.Name = source.Name
-	schemaLoadBalancerCreateRequest.LoadBalancerType = anyFromLoadBalancerType(source.LoadBalancerType)
+	schemaLoadBalancerCreateRequest.LoadBalancerType = c.pHcloudLoadBalancerTypeToSchemaIDOrName(source.LoadBalancerType)
 	schemaLoadBalancerCreateRequest.Algorithm = c.pHcloudLoadBalancerAlgorithmToPSchemaLoadBalancerCreateRequestAlgorithm(source.Algorithm)
 	schemaLoadBalancerCreateRequest.Location = c.pHcloudLocationToPString(source.Location)
 	schemaLoadBalancerCreateRequest.NetworkZone = stringPtrFromNetworkZone(source.NetworkZone)
@@ -1732,6 +1732,16 @@ func (c *converterImpl) pHcloudLoadBalancerToInt64(source *LoadBalancer) int64 {
 	}
 	return xint64
 }
+func (c *converterImpl) pHcloudLoadBalancerTypeToSchemaIDOrName(source *LoadBalancerType) schema.IDOrName {
+	var schemaIDOrName schema.IDOrName
+	if source != nil {
+		var schemaIDOrName2 schema.IDOrName
+		schemaIDOrName2.ID = (*source).ID
+		schemaIDOrName2.Name = (*source).Name
+		schemaIDOrName = schemaIDOrName2
+	}
+	return schemaIDOrName
+}
 func (c *converterImpl) pHcloudLoadBalancerUpdateServiceOptsHTTPToPSchemaLoadBalancerActionUpdateServiceRequestHTTP(source *LoadBalancerUpdateServiceOptsHTTP) *schema.LoadBalancerActionUpdateServiceRequestHTTP {
 	var pSchemaLoadBalancerActionUpdateServiceRequestHTTP *schema.LoadBalancerActionUpdateServiceRequestHTTP
 	if source != nil {


Test patch:
diff --git a/hcloud/load_balancer_test.go b/hcloud/load_balancer_test.go
index f9190b2..2f18efe 100644
--- a/hcloud/load_balancer_test.go
+++ b/hcloud/load_balancer_test.go
@@ -175,7 +175,7 @@ func TestLoadBalancerCreate(t *testing.T) {
 		}
 		expectedReqBody := schema.LoadBalancerCreateRequest{
 			Name:             "load-balancer",
-			LoadBalancerType: "lb1",
+			LoadBalancerType: schema.IDOrName{Name: "lb1"},
 			Algorithm: &schema.LoadBalancerCreateRequestAlgorithm{
 				Type: "round_robin",
 			},
@@ -813,7 +813,7 @@ func TestLoadBalancerClientChangeType(t *testing.T) {
 			if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
 				t.Fatal(err)
 			}
-			if id, ok := reqBody.LoadBalancerType.(float64); !ok || id != 1 {
+			if reqBody.LoadBalancerType.ID != 1 {
 				t.Errorf("unexpected Load Balancer type ID: %v", reqBody.LoadBalancerType)
 			}
 			json.NewEncoder(w).Encode(schema.LoadBalancerActionChangeTypeResponse{
@@ -844,7 +844,7 @@ func TestLoadBalancerClientChangeType(t *testing.T) {
 			if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
 				t.Fatal(err)
 			}
-			if name, ok := reqBody.LoadBalancerType.(string); !ok || name != "type" {
+			if reqBody.LoadBalancerType.Name != "type" {
 				t.Errorf("unexpected Load Balancer type name: %v", reqBody.LoadBalancerType)
 			}
 			json.NewEncoder(w).Encode(schema.LoadBalancerActionChangeTypeResponse{
diff --git a/hcloud/schema/id_or_name_test.go b/hcloud/schema/id_or_name_test.go
new file mode 100644
index 0000000..54e20a5
--- /dev/null
+++ b/hcloud/schema/id_or_name_test.go
@@ -0,0 +1,105 @@
+package schema
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestIDOrNameMarshall(t *testing.T) {
+	t.Run("id", func(t *testing.T) {
+		i := IDOrName{ID: 1}
+
+		got, err := i.MarshalJSON()
+		require.NoError(t, err)
+		require.Equal(t, `1`, string(got))
+	})
+
+	t.Run("name", func(t *testing.T) {
+		i := IDOrName{Name: "name"}
+
+		got, err := i.MarshalJSON()
+		require.NoError(t, err)
+		require.Equal(t, `"name"`, string(got))
+	})
+
+	t.Run("id and name", func(t *testing.T) {
+		i := IDOrName{ID: 1, Name: "name"}
+
+		got, err := i.MarshalJSON()
+		require.NoError(t, err)
+		require.Equal(t, `1`, string(got))
+	})
+
+	t.Run("null", func(t *testing.T) {
+		i := IDOrName{}
+
+		got, err := i.MarshalJSON()
+		require.NoError(t, err)
+		require.Equal(t, `null`, string(got))
+	})
+}
+
+func TestIDOrNameUnMarshall(t *testing.T) {
+	t.Run("id", func(t *testing.T) {
+		i := IDOrName{}
+
+		err := i.UnmarshalJSON([]byte(`1`))
+		require.NoError(t, err)
+		require.Equal(t, IDOrName{ID: 1}, i)
+	})
+	t.Run("name", func(t *testing.T) {
+		i := IDOrName{}
+
+		err := i.UnmarshalJSON([]byte(`"name"`))
+		require.NoError(t, err)
+		require.Equal(t, IDOrName{Name: "name"}, i)
+	})
+	t.Run("id string", func(t *testing.T) {
+		i := IDOrName{}
+
+		err := i.UnmarshalJSON([]byte(`"1"`))
+		require.NoError(t, err)
+		require.Equal(t, IDOrName{ID: 1}, i)
+	})
+	t.Run("id float", func(t *testing.T) {
+		i := IDOrName{}
+
+		err := i.UnmarshalJSON([]byte(`1.0`))
+		require.EqualError(t, err, "json: cannot unmarshal 1.0 into Go value of type schema.IDOrName")
+	})
+	t.Run("null", func(t *testing.T) {
+		i := IDOrName{}
+
+		err := i.UnmarshalJSON([]byte(`null`))
+		require.EqualError(t, err, "json: cannot unmarshal null into Go value of type schema.IDOrName")
+	})
+}
+
+func TestIDOrName(t *testing.T) {
+	// Make sure the behavior does not change from the use of an interface{}.
+	type FakeRequest struct {
+		Old interface{} `json:"old"`
+		New IDOrName    `json:"new"`
+	}
+
+	t.Run("null", func(t *testing.T) {
+		o := FakeRequest{}
+		body, err := json.Marshal(o)
+		require.NoError(t, err)
+		require.Equal(t, `{"old":null,"new":null}`, string(body))
+	})
+	t.Run("id", func(t *testing.T) {
+		o := FakeRequest{Old: int64(1), New: IDOrName{ID: 1}}
+		body, err := json.Marshal(o)
+		require.NoError(t, err)
+		require.Equal(t, `{"old":1,"new":1}`, string(body))
+	})
+	t.Run("name", func(t *testing.T) {
+		o := FakeRequest{Old: "name", New: IDOrName{Name: "name"}}
+		body, err := json.Marshal(o)
+		require.NoError(t, err)
+		require.Equal(t, `{"old":"name","new":"name"}`, string(body))
+	})
+}
diff --git a/hcloud/schema_test.go b/hcloud/schema_test.go
index 3ee2779..3bec207 100644
--- a/hcloud/schema_test.go
+++ b/hcloud/schema_test.go
@@ -1508,7 +1508,7 @@ func TestLoadBalancerCreateOptsToSchema(t *testing.T) {
 			},
 			Request: schema.LoadBalancerCreateRequest{
 				Name:             "test",
-				LoadBalancerType: "lb11",
+				LoadBalancerType: schema.IDOrName{Name: "lb11"},
 				Algorithm: &schema.LoadBalancerCreateRequestAlgorithm{
 					Type: string(LoadBalancerAlgorithmTypeRoundRobin),
 				},
@@ -1593,7 +1593,7 @@ func TestLoadBalancerCreateOptsToSchema(t *testing.T) {
 			},
 			Request: schema.LoadBalancerCreateRequest{
 				Name:             "test",
-				LoadBalancerType: "lb11",
+				LoadBalancerType: schema.IDOrName{Name: "lb11"},
 				Algorithm: &schema.LoadBalancerCreateRequestAlgorithm{
 					Type: string(LoadBalancerAlgorithmTypeRoundRobin),
 				},
diff --git a/hcloud/server_test.go b/hcloud/server_test.go
index 0a01513..f1e4535 100644
--- a/hcloud/server_test.go
+++ b/hcloud/server_test.go
@@ -1468,7 +1468,7 @@ func TestServerClientRebuild(t *testing.T) {
 			if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
 				t.Fatal(err)
 			}
-			if id, ok := reqBody.Image.(float64); !ok || id != 1 {
+			if reqBody.Image.ID != 1 {
 				t.Errorf("unexpected image ID: %v", reqBody.Image)
 			}
 			json.NewEncoder(w).Encode(schema.ServerActionRebuildResponse{
@@ -1499,7 +1499,7 @@ func TestServerClientRebuild(t *testing.T) {
 			if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
 				t.Fatal(err)
 			}
-			if name, ok := reqBody.Image.(string); !ok || name != "debian-9" {
+			if reqBody.Image.Name != "debian-9" {
 				t.Errorf("unexpected image name: %v", reqBody.Image)
 			}
 			json.NewEncoder(w).Encode(schema.ServerActionRebuildResponse{
@@ -1537,7 +1537,7 @@ func TestServerClientRebuildWithResult(t *testing.T) {
 			if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
 				t.Fatal(err)
 			}
-			if id, ok := reqBody.Image.(float64); !ok || id != 1 {
+			if reqBody.Image.ID != 1 {
 				t.Errorf("unexpected image ID: %v", reqBody.Image)
 			}
 			json.NewEncoder(w).Encode(schema.ServerActionRebuildResponse{
@@ -1572,7 +1572,7 @@ func TestServerClientRebuildWithResult(t *testing.T) {
 			if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
 				t.Fatal(err)
 			}
-			if name, ok := reqBody.Image.(string); !ok || name != "debian-9" {
+			if reqBody.Image.Name != "debian-9" {
 				t.Errorf("unexpected image name: %v", reqBody.Image)
 			}
 			json.NewEncoder(w).Encode(schema.ServerActionRebuildResponse{
@@ -1614,7 +1614,7 @@ func TestServerClientAttachISO(t *testing.T) {
 			if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
 				t.Fatal(err)
 			}
-			if id, ok := reqBody.ISO.(float64); !ok || id != 1 {
+			if reqBody.ISO.ID != 1 {
 				t.Errorf("unexpected ISO ID: %v", reqBody.ISO)
 			}
 			json.NewEncoder(w).Encode(schema.ServerActionAttachISOResponse{
@@ -1643,7 +1643,7 @@ func TestServerClientAttachISO(t *testing.T) {
 			if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
 				t.Fatal(err)
 			}
-			if name, ok := reqBody.ISO.(string); !ok || name != "debian.iso" {
+			if reqBody.ISO.Name != "debian.iso" {
 				t.Errorf("unexpected ISO name: %v", reqBody.ISO)
 			}
 			json.NewEncoder(w).Encode(schema.ServerActionAttachISOResponse{
@@ -1757,7 +1757,7 @@ func TestServerClientChangeType(t *testing.T) {
 			if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
 				t.Fatal(err)
 			}
-			if id, ok := reqBody.ServerType.(float64); !ok || id != 1 {
+			if reqBody.ServerType.ID != 1 {
 				t.Errorf("unexpected server type ID: %v", reqBody.ServerType)
 			}
 			if !reqBody.UpgradeDisk {
@@ -1792,7 +1792,7 @@ func TestServerClientChangeType(t *testing.T) {
 			if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
 				t.Fatal(err)
 			}
-			if name, ok := reqBody.ServerType.(string); !ok || name != "type" {
+			if reqBody.ServerType.Name != "type" {
 				t.Errorf("unexpected server type name: %v", reqBody.ServerType)
 			}
 			if !reqBody.UpgradeDisk {
diff --git a/hcloud/volume_test.go b/hcloud/volume_test.go
index 8e2255a..de05b94 100644
--- a/hcloud/volume_test.go
+++ b/hcloud/volume_test.go
@@ -319,7 +319,7 @@ func TestVolumeClientCreateWithLocation(t *testing.T) {
 		if reqBody.Size != 42 {
 			t.Errorf("unexpected volume size in request: %v", reqBody.Size)
 		}
-		if reqBody.Location != float64(1) {
+		if reqBody.Location.ID != 1 {
 			t.Errorf("unexpected volume location in request: %v", reqBody.Location)
 		}
 		if reqBody.Server != nil {