You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by sr...@apache.org on 2023/01/11 17:05:44 UTC

[trafficcontrol] branch master updated: Fix edge cases for DSR creation, update, and deletion (#7268)

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

srijeet0406 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git


The following commit(s) were added to refs/heads/master by this push:
     new b1791de5da Fix edge cases for DSR creation, update, and deletion (#7268)
b1791de5da is described below

commit b1791de5da52032ecf3fcc447e1b905b6d866d95
Author: ocket8888 <oc...@apache.org>
AuthorDate: Wed Jan 11 10:05:36 2023 -0700

    Fix edge cases for DSR creation, update, and deletion (#7268)
    
    * Switch to non-pointer receivers on all upgrade/downgrade methods of DSes
    
    6 out of 8 upgrade/downgrade methods (2 of 4 downgrades and 4 of 4
    upgrades) already used a non-pointer receiver. Since the methods don't
    need to mutate the structure on which they are called, we can make life
    simpler by just using a regular receiver.
    
    * Fix Internal Server Error when creating a DSR in APIv3 without the required "deliveryservice" property
    
    * Fix creating and deleting DSRs in APIv3 returning APIv4.0 structures
    
    * Replace a dozen type-specific utility functions with two generic ones
    
    * Export copy utilities
    
    * Move copy utilities to the utils package
    
    * Add deprecation notices; everything should just use Ptr
    
    * foo
    
    * Add test coverage for the time pointer function
    
    * Switch tests to runnable examples
    
    * Add methods to coalesce DSes and DSRs from "nullable" v3.x structures to concrete structures
    
    * Add integration tests
    
    * Fix inaccurate godoc comment
    
    * typo
    
    * Remove bad test
    
    * Remove now-unnecessary ToConcrete methods
---
 lib/go-tc/copy.go                                  | 112 -----------
 lib/go-tc/copy_test.go                             | 166 ---------------
 lib/go-tc/deliveryservice_requests.go              |  45 +++--
 lib/go-tc/deliveryservices.go                      | 224 ++++++++++-----------
 lib/go-tc/users.go                                 | 132 ++++++------
 lib/go-util/ptr.go                                 | 109 ++++++++--
 lib/go-util/ptr_test.go                            |  42 +++-
 .../api/v3/deliveryservice_requests_test.go        |  45 ++++-
 .../deliveryservice/request/requests.go            |  10 +-
 .../deliveryservice/request/validate.go            |   3 +
 10 files changed, 394 insertions(+), 494 deletions(-)

diff --git a/lib/go-tc/copy.go b/lib/go-tc/copy.go
deleted file mode 100644
index 57152635d3..0000000000
--- a/lib/go-tc/copy.go
+++ /dev/null
@@ -1,112 +0,0 @@
-package tc
-
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-// copyStringIfNotNil makes a deep copy of s - unless it's nil, in which case it
-// just returns nil.
-func copyStringIfNotNil(s *string) *string {
-	if s == nil {
-		return nil
-	}
-	ret := new(string)
-	*ret = *s
-	return ret
-}
-
-// coalesceString coalesces a possibly nil pointer to a string to a concrete
-// string, using the provided default value in case `nil` is encountered.
-//
-// This can be thought of as roughly the inverse of
-// github.com/apache/trafficcontrol/lib/go-util.StrPtr.
-func coalesceString(s *string, def string) string {
-	if s == nil {
-		return def
-	}
-	return *s
-}
-
-// copyIntIfNotNil makes a deep copy of i - unless it's nil, in which case it
-// just returns nil.
-func copyIntIfNotNil(i *int) *int {
-	if i == nil {
-		return nil
-	}
-	ret := new(int)
-	*ret = *i
-	return ret
-}
-
-// coalesceInt coalesces a possibly nil pointer to an integer to a concrete
-// integer, using the provided default value in case `nil` is encountered.
-//
-// This can be thought of as roughly the inverse of
-// github.com/apache/trafficcontrol/lib/go-util.IntPtr.
-func coalesceInt(i *int, def int) int {
-	if i == nil {
-		return def
-	}
-	return *i
-}
-
-// copyBoolIfNotNil makes a deep copy of b - unless it's nil, in which case it
-// just returns nil.
-func copyBoolIfNotNil(b *bool) *bool {
-	if b == nil {
-		return nil
-	}
-	ret := new(bool)
-	*ret = *b
-	return ret
-}
-
-// coalesceBool coalesces a possibly nil pointer to a boolean to a concrete
-// boolean, using the provided default value in case `nil` is encountered.
-//
-// This can be thought of as roughly the inverse of
-// github.com/apache/trafficcontrol/lib/go-util.BoolPtr.
-func coalesceBool(b *bool, def bool) bool {
-	if b == nil {
-		return def
-	}
-	return *b
-}
-
-// copyFloatIfNotNil makes a deep copy of f - unless it's nil, in which case it
-// just returns nil.
-func copyFloatIfNotNil(f *float64) *float64 {
-	if f == nil {
-		return nil
-	}
-	ret := new(float64)
-	*ret = *f
-	return ret
-}
-
-// coalesceFloat coalesces a possibly nil pointer to a float64 to a concrete
-// float64, using the provided default value in case `nil` is encountered.
-//
-// This can be thought of as roughly the inverse of
-// github.com/apache/trafficcontrol/lib/go-util.FloatPtr.
-func coalesceFloat(f *float64, def float64) float64 {
-	if f == nil {
-		return def
-	}
-	return *f
-}
diff --git a/lib/go-tc/copy_test.go b/lib/go-tc/copy_test.go
deleted file mode 100644
index b4afb30b57..0000000000
--- a/lib/go-tc/copy_test.go
+++ /dev/null
@@ -1,166 +0,0 @@
-package tc
-
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import "testing"
-
-func TestCopyIntIfNotNil(t *testing.T) {
-	var i *int
-	copiedI := copyIntIfNotNil(i)
-	if copiedI != nil {
-		t.Errorf("Copying a nil int should've given nil, got: %d", *copiedI)
-	}
-	i = new(int)
-	*i = 9000
-	copiedI = copyIntIfNotNil(i)
-	if copiedI == nil {
-		t.Errorf("Copied pointer to %d was nil", *i)
-	} else {
-		if *copiedI != *i {
-			t.Errorf("Incorrectly copied int pointer; expected: %d, got: %d", *i, *copiedI)
-		}
-		*i = 9001
-		if *copiedI == *i {
-			t.Error("Expected copy to be 'deep' but modifying the original int changed the copy")
-		}
-	}
-}
-
-func TestCoalesceInt(t *testing.T) {
-	var i *int
-	copiedI := coalesceInt(i, 9000)
-	if copiedI != 9000 {
-		t.Errorf("Coalescing a nil int should've given the default value, got: %d", copiedI)
-	}
-	i = new(int)
-	*i = 9001
-	copiedI = coalesceInt(i, 9000)
-	if copiedI != 9001 {
-		t.Errorf("Coalescing a non-nil int should've given %d, got: %d", *i, copiedI)
-	}
-}
-
-func TestCopyFloatIfNotNil(t *testing.T) {
-	var f *float64
-	copiedF := copyFloatIfNotNil(f)
-	if copiedF != nil {
-		t.Errorf("Copying a nil float should've given nil, got: %f", *copiedF)
-	}
-	f = new(float64)
-	*f = 9000
-	copiedF = copyFloatIfNotNil(f)
-	if copiedF == nil {
-		t.Errorf("Copied pointer to %f was nil", *f)
-	} else {
-		if *copiedF != *f {
-			t.Errorf("Incorrectly copied float64 pointer; expected: %f, got: %f", *f, *copiedF)
-		}
-		*f = 9001
-		if *copiedF == *f {
-			t.Error("Expected copy to be 'deep' but modifying the original float changed the copy")
-		}
-	}
-}
-
-func TestCoalesceFloat(t *testing.T) {
-	var f *float64
-	copiedF := coalesceFloat(f, 9000)
-	if copiedF != 9000 {
-		t.Errorf("Coalescing a nil float should've given the default value, got: %f", copiedF)
-	}
-	f = new(float64)
-	*f = 9001
-	copiedF = coalesceFloat(f, 9000)
-	if copiedF != 9001 {
-		t.Errorf("Coalescing a non-nil float should've given %f, got: %f", *f, copiedF)
-	}
-}
-
-func TestCopyBoolIfNotNil(t *testing.T) {
-	var b *bool
-	copiedB := copyBoolIfNotNil(b)
-	if copiedB != nil {
-		t.Errorf("Copying a nil boolean should've given nil, got: %t", *copiedB)
-	}
-	b = new(bool)
-	*b = true
-	copiedB = copyBoolIfNotNil(b)
-	if copiedB == nil {
-		t.Errorf("Copied pointer to %t was nil", *b)
-	} else {
-		if *copiedB != *b {
-			t.Errorf("Incorrectly copied bool pointer; expected: %t, got: %t", *b, *copiedB)
-		}
-		*b = false
-		if *copiedB == *b {
-			t.Error("Expected copy to be 'deep' but modifying the original bool changed the copy")
-		}
-	}
-}
-
-func TestCoalesceBool(t *testing.T) {
-	var b *bool
-	copiedB := coalesceBool(b, true)
-	if copiedB != true {
-		t.Errorf("Coalescing a nil boolean should've given the default value, got: %t", copiedB)
-	}
-	b = new(bool)
-	*b = false
-	copiedB = coalesceBool(b, true)
-	if copiedB != false {
-		t.Errorf("Coalescing a non-nil bool should've given %t, got: %t", *b, copiedB)
-	}
-}
-
-func TestCopyStringIfNotNil(t *testing.T) {
-	var s *string
-	copiedS := copyStringIfNotNil(s)
-	if copiedS != nil {
-		t.Errorf("Copying a nil string should've given nil, got: %s", *copiedS)
-	}
-	s = new(string)
-	*s = "test string"
-	copiedS = copyStringIfNotNil(s)
-	if copiedS == nil {
-		t.Errorf("Copied pointer to '%s' was nil", *s)
-	} else {
-		if *copiedS != *s {
-			t.Errorf("Incorrectly copied string pointer; expected: '%s', got: '%s'", *s, *copiedS)
-		}
-		*s = "a different test string"
-		if *copiedS == *s {
-			t.Error("Expected copy to be 'deep' but modifying the original string changed the copy")
-		}
-	}
-}
-
-func TestCoalesceString(t *testing.T) {
-	var s *string
-	copiedS := coalesceString(s, "test")
-	if copiedS != "test" {
-		t.Errorf("Coalescing a nil string should've given the default value, got: %s", copiedS)
-	}
-	s = new(string)
-	*s = "quest"
-	copiedS = coalesceString(s, "test")
-	if copiedS != "quest" {
-		t.Errorf("Coalescing a non-nil string should've given %s, got: %s", *s, copiedS)
-	}
-}
diff --git a/lib/go-tc/deliveryservice_requests.go b/lib/go-tc/deliveryservice_requests.go
index 817cabc0c3..f335769256 100644
--- a/lib/go-tc/deliveryservice_requests.go
+++ b/lib/go-tc/deliveryservice_requests.go
@@ -401,6 +401,9 @@ func (o *OriginHeaders) UnmarshalJSON(data []byte) error {
 
 // DeliveryServiceRequest is used as part of the workflow to create,
 // modify, or delete a delivery service.
+//
+// Deprecated: Don't ever use this, even in legacy code if you can help it. It
+// shouldn't still exist already, but will nevertheless be removed soon.
 type DeliveryServiceRequest struct {
 	AssigneeID      int             `json:"assigneeId,omitempty"`
 	Assignee        string          `json:"assignee,omitempty"`
@@ -419,6 +422,8 @@ type DeliveryServiceRequest struct {
 
 // DeliveryServiceRequestNullable is used as part of the workflow to create,
 // modify, or delete a delivery service.
+//
+// Deprecated: This structure is only used in legacy API versions.
 type DeliveryServiceRequestNullable struct {
 	AssigneeID      *int                        `json:"assigneeId,omitempty" db:"assignee_id"`
 	Assignee        *string                     `json:"assignee,omitempty"`
@@ -439,15 +444,15 @@ type DeliveryServiceRequestNullable struct {
 // Note that this function does a shallow copy of the requested and original Delivery Service structures.
 func (dsr DeliveryServiceRequestV41) Downgrade() DeliveryServiceRequestV40 {
 	var dsrV40 DeliveryServiceRequestV40
-	dsrV40.Assignee = copyStringIfNotNil(dsr.Assignee)
-	dsrV40.AssigneeID = copyIntIfNotNil(dsr.AssigneeID)
+	dsrV40.Assignee = util.CopyIfNotNil(dsr.Assignee)
+	dsrV40.AssigneeID = util.CopyIfNotNil(dsr.AssigneeID)
 	dsrV40.Author = dsr.Author
-	dsrV40.AuthorID = copyIntIfNotNil(dsr.AuthorID)
+	dsrV40.AuthorID = util.CopyIfNotNil(dsr.AuthorID)
 	dsrV40.ChangeType = dsr.ChangeType
 	dsrV40.CreatedAt = dsr.CreatedAt
-	dsrV40.ID = copyIntIfNotNil(dsr.ID)
+	dsrV40.ID = util.CopyIfNotNil(dsr.ID)
 	dsrV40.LastEditedBy = dsr.LastEditedBy
-	dsrV40.LastEditedByID = copyIntIfNotNil(dsr.LastEditedByID)
+	dsrV40.LastEditedByID = util.CopyIfNotNil(dsr.LastEditedByID)
 	dsrV40.LastUpdated = dsr.LastUpdated
 	if dsr.Original != nil {
 		dsrV40.Original = new(DeliveryServiceV40)
@@ -466,15 +471,15 @@ func (dsr DeliveryServiceRequestV41) Downgrade() DeliveryServiceRequestV40 {
 // Note that this function does a shallow copy of the requested and original Delivery Service structures.
 func (dsrV40 DeliveryServiceRequestV40) Upgrade() DeliveryServiceRequestV41 {
 	var dsrV4 DeliveryServiceRequestV41
-	dsrV4.Assignee = copyStringIfNotNil(dsrV40.Assignee)
-	dsrV4.AssigneeID = copyIntIfNotNil(dsrV40.AssigneeID)
+	dsrV4.Assignee = util.CopyIfNotNil(dsrV40.Assignee)
+	dsrV4.AssigneeID = util.CopyIfNotNil(dsrV40.AssigneeID)
 	dsrV4.Author = dsrV40.Author
-	dsrV4.AuthorID = copyIntIfNotNil(dsrV40.AuthorID)
+	dsrV4.AuthorID = util.CopyIfNotNil(dsrV40.AuthorID)
 	dsrV4.ChangeType = dsrV40.ChangeType
 	dsrV4.CreatedAt = dsrV40.CreatedAt
-	dsrV4.ID = copyIntIfNotNil(dsrV40.ID)
+	dsrV4.ID = util.CopyIfNotNil(dsrV40.ID)
 	dsrV4.LastEditedBy = dsrV40.LastEditedBy
-	dsrV4.LastEditedByID = copyIntIfNotNil(dsrV40.LastEditedByID)
+	dsrV4.LastEditedByID = util.CopyIfNotNil(dsrV40.LastEditedByID)
 	dsrV4.LastUpdated = dsrV40.LastUpdated
 	if dsrV40.Original != nil {
 		dsrV4.Original = new(DeliveryServiceV41)
@@ -1089,15 +1094,15 @@ type DeliveryServiceRequestV5 = DeliveryServiceRequestV50
 // also deep).
 func (dsr DeliveryServiceRequestV5) Downgrade() DeliveryServiceRequestV4 {
 	downgraded := DeliveryServiceRequestV4{
-		Assignee:       copyStringIfNotNil(dsr.Assignee),
-		AssigneeID:     copyIntIfNotNil(dsr.AssigneeID),
+		Assignee:       util.CopyIfNotNil(dsr.Assignee),
+		AssigneeID:     util.CopyIfNotNil(dsr.AssigneeID),
 		Author:         dsr.Author,
-		AuthorID:       copyIntIfNotNil(dsr.AuthorID),
+		AuthorID:       util.CopyIfNotNil(dsr.AuthorID),
 		ChangeType:     dsr.ChangeType,
 		CreatedAt:      dsr.CreatedAt,
-		ID:             copyIntIfNotNil(dsr.ID),
+		ID:             util.CopyIfNotNil(dsr.ID),
 		LastEditedBy:   dsr.LastEditedBy,
-		LastEditedByID: copyIntIfNotNil(dsr.LastEditedByID),
+		LastEditedByID: util.CopyIfNotNil(dsr.LastEditedByID),
 		LastUpdated:    dsr.LastUpdated,
 		Status:         dsr.Status,
 		XMLID:          dsr.XMLID,
@@ -1124,15 +1129,15 @@ func (dsr DeliveryServiceRequestV5) Downgrade() DeliveryServiceRequestV4 {
 // also deep).
 func (dsr DeliveryServiceRequestV4) Upgrade() DeliveryServiceRequestV5 {
 	upgraded := DeliveryServiceRequestV5{
-		Assignee:       copyStringIfNotNil(dsr.Assignee),
-		AssigneeID:     copyIntIfNotNil(dsr.AssigneeID),
+		Assignee:       util.CopyIfNotNil(dsr.Assignee),
+		AssigneeID:     util.CopyIfNotNil(dsr.AssigneeID),
 		Author:         dsr.Author,
-		AuthorID:       copyIntIfNotNil(dsr.AuthorID),
+		AuthorID:       util.CopyIfNotNil(dsr.AuthorID),
 		ChangeType:     dsr.ChangeType,
 		CreatedAt:      dsr.CreatedAt,
-		ID:             copyIntIfNotNil(dsr.ID),
+		ID:             util.CopyIfNotNil(dsr.ID),
 		LastEditedBy:   dsr.LastEditedBy,
-		LastEditedByID: copyIntIfNotNil(dsr.LastEditedByID),
+		LastEditedByID: util.CopyIfNotNil(dsr.LastEditedByID),
 		LastUpdated:    dsr.LastUpdated,
 		Status:         dsr.Status,
 		XMLID:          dsr.XMLID,
diff --git a/lib/go-tc/deliveryservices.go b/lib/go-tc/deliveryservices.go
index daf61e8f9b..2cdedb536a 100644
--- a/lib/go-tc/deliveryservices.go
+++ b/lib/go-tc/deliveryservices.go
@@ -121,7 +121,7 @@ type CreateDeliveryServiceNullableResponse struct {
 	Alerts
 }
 
-// UpdateDeliveryServiceNullableResponse oughly models the structure of
+// UpdateDeliveryServiceNullableResponse roughly models the structure of
 // responses from Traffic Ops to PUT requests made to its
 // /deliveryservices/{{ID}} API endpoint.
 //
@@ -422,7 +422,7 @@ func tlsVersionsAlerts(versions []string, protocol int) Alerts {
 // to CDN operation, but can, in fact, work.
 func (ds DeliveryServiceV40) TLSVersionsAlerts() Alerts {
 	vers := ds.TLSVersions
-	return tlsVersionsAlerts(vers, coalesceInt(ds.Protocol, 3))
+	return tlsVersionsAlerts(vers, util.Coalesce(ds.Protocol, 3))
 }
 
 // TLSVersionsAlerts generates warning-level alerts for the Delivery Service's
@@ -911,7 +911,7 @@ func (ds *DeliveryServiceV4) RemoveLD1AndLD2() DeliveryServiceV4 {
 }
 
 // DowngradeToV31 converts a 4.x DS to a 3.1 DS.
-func (ds *DeliveryServiceV4) DowngradeToV31() DeliveryServiceNullableV30 {
+func (ds DeliveryServiceV4) DowngradeToV31() DeliveryServiceNullableV30 {
 	nullableFields := ds.DeliveryServiceNullableFieldsV11
 	geoLimitCountries := ([]string)(ds.GeoLimitCountries)
 	geo := strings.Join(geoLimitCountries, ",")
@@ -939,7 +939,7 @@ func (ds *DeliveryServiceV4) DowngradeToV31() DeliveryServiceNullableV30 {
 }
 
 // UpgradeToV4 converts the 3.x DS to a 4.x DS.
-func (ds *DeliveryServiceNullableV30) UpgradeToV4() DeliveryServiceV4 {
+func (ds DeliveryServiceNullableV30) UpgradeToV4() DeliveryServiceV4 {
 	var geo GeoLimitCountriesType
 	if ds.GeoLimitCountries != nil {
 		str := *ds.GeoLimitCountries
@@ -1509,7 +1509,7 @@ type DeliveryServiceV5 = DeliveryServiceV50
 // it ONLY creates warnings based on conditions that are possibly detrimental
 // to CDN operation, but can, in fact, work.
 func (ds DeliveryServiceV5) TLSVersionsAlerts() Alerts {
-	return tlsVersionsAlerts(ds.TLSVersions, coalesceInt(ds.Protocol, 3))
+	return tlsVersionsAlerts(ds.TLSVersions, util.Coalesce(ds.Protocol, 3))
 }
 
 // Value implements the database/sql/driver.Valuer interface by marshaling the
@@ -1532,80 +1532,80 @@ func (ds DeliveryServiceV5) Downgrade() DeliveryServiceV4 {
 	downgraded := DeliveryServiceV4{
 		DeliveryServiceV40: DeliveryServiceV40{
 			DeliveryServiceFieldsV31: DeliveryServiceFieldsV31{
-				MaxRequestHeaderBytes: copyIntIfNotNil(ds.MaxRequestHeaderBytes),
+				MaxRequestHeaderBytes: util.CopyIfNotNil(ds.MaxRequestHeaderBytes),
 			},
 			DeliveryServiceFieldsV30: DeliveryServiceFieldsV30{
-				FirstHeaderRewrite: copyStringIfNotNil(ds.FirstHeaderRewrite),
-				InnerHeaderRewrite: copyStringIfNotNil(ds.InnerHeaderRewrite),
-				LastHeaderRewrite:  copyStringIfNotNil(ds.LastHeaderRewrite),
-				ServiceCategory:    copyStringIfNotNil(ds.ServiceCategory),
-				Topology:           copyStringIfNotNil(ds.Topology),
+				FirstHeaderRewrite: util.CopyIfNotNil(ds.FirstHeaderRewrite),
+				InnerHeaderRewrite: util.CopyIfNotNil(ds.InnerHeaderRewrite),
+				LastHeaderRewrite:  util.CopyIfNotNil(ds.LastHeaderRewrite),
+				ServiceCategory:    util.CopyIfNotNil(ds.ServiceCategory),
+				Topology:           util.CopyIfNotNil(ds.Topology),
 			},
 			DeliveryServiceFieldsV15: DeliveryServiceFieldsV15{
 				EcsEnabled:          ds.EcsEnabled,
-				RangeSliceBlockSize: copyIntIfNotNil(ds.RangeSliceBlockSize),
+				RangeSliceBlockSize: util.CopyIfNotNil(ds.RangeSliceBlockSize),
 			},
 			DeliveryServiceFieldsV14: DeliveryServiceFieldsV14{
 				ConsistentHashQueryParams: make([]string, len(ds.ConsistentHashQueryParams)),
-				ConsistentHashRegex:       copyStringIfNotNil(ds.ConsistentHashRegex),
-				MaxOriginConnections:      copyIntIfNotNil(ds.MaxOriginConnections),
+				ConsistentHashRegex:       util.CopyIfNotNil(ds.ConsistentHashRegex),
+				MaxOriginConnections:      util.CopyIfNotNil(ds.MaxOriginConnections),
 			},
 			DeliveryServiceFieldsV13: DeliveryServiceFieldsV13{
 				DeepCachingType:   new(DeepCachingType),
-				FQPacingRate:      copyIntIfNotNil(ds.FQPacingRate),
-				SigningAlgorithm:  copyStringIfNotNil(ds.SigningAlgorithm),
-				Tenant:            copyStringIfNotNil(ds.Tenant),
-				TRResponseHeaders: copyStringIfNotNil(ds.TRResponseHeaders),
-				TRRequestHeaders:  copyStringIfNotNil(ds.TRRequestHeaders),
+				FQPacingRate:      util.CopyIfNotNil(ds.FQPacingRate),
+				SigningAlgorithm:  util.CopyIfNotNil(ds.SigningAlgorithm),
+				Tenant:            util.CopyIfNotNil(ds.Tenant),
+				TRResponseHeaders: util.CopyIfNotNil(ds.TRResponseHeaders),
+				TRRequestHeaders:  util.CopyIfNotNil(ds.TRRequestHeaders),
 			},
 			DeliveryServiceNullableFieldsV11: DeliveryServiceNullableFieldsV11{
 				Active:                   new(bool),
 				AnonymousBlockingEnabled: util.BoolPtr(ds.AnonymousBlockingEnabled),
-				CCRDNSTTL:                copyIntIfNotNil(ds.CCRDNSTTL),
+				CCRDNSTTL:                util.CopyIfNotNil(ds.CCRDNSTTL),
 				CDNID:                    util.IntPtr(ds.CDNID),
-				CDNName:                  copyStringIfNotNil(ds.CDNName),
-				CheckPath:                copyStringIfNotNil(ds.CheckPath),
+				CDNName:                  util.CopyIfNotNil(ds.CDNName),
+				CheckPath:                util.CopyIfNotNil(ds.CheckPath),
 				DisplayName:              util.StrPtr(ds.DisplayName),
-				DNSBypassCNAME:           copyStringIfNotNil(ds.DNSBypassCNAME),
-				DNSBypassIP:              copyStringIfNotNil(ds.DNSBypassIP),
-				DNSBypassIP6:             copyStringIfNotNil(ds.DNSBypassIP6),
-				DNSBypassTTL:             copyIntIfNotNil(ds.DNSBypassTTL),
+				DNSBypassCNAME:           util.CopyIfNotNil(ds.DNSBypassCNAME),
+				DNSBypassIP:              util.CopyIfNotNil(ds.DNSBypassIP),
+				DNSBypassIP6:             util.CopyIfNotNil(ds.DNSBypassIP6),
+				DNSBypassTTL:             util.CopyIfNotNil(ds.DNSBypassTTL),
 				DSCP:                     util.IntPtr(ds.DSCP),
-				EdgeHeaderRewrite:        copyStringIfNotNil(ds.EdgeHeaderRewrite),
+				EdgeHeaderRewrite:        util.CopyIfNotNil(ds.EdgeHeaderRewrite),
 				GeoLimit:                 util.IntPtr(ds.GeoLimit),
-				GeoLimitRedirectURL:      copyStringIfNotNil(ds.GeoLimitRedirectURL),
+				GeoLimitRedirectURL:      util.CopyIfNotNil(ds.GeoLimitRedirectURL),
 				GeoProvider:              util.IntPtr(ds.GeoProvider),
-				GlobalMaxMBPS:            copyIntIfNotNil(ds.GlobalMaxMBPS),
-				GlobalMaxTPS:             copyIntIfNotNil(ds.GlobalMaxTPS),
-				HTTPBypassFQDN:           copyStringIfNotNil(ds.HTTPBypassFQDN),
-				ID:                       copyIntIfNotNil(ds.ID),
-				InfoURL:                  copyStringIfNotNil(ds.InfoURL),
-				InitialDispersion:        copyIntIfNotNil(ds.InitialDispersion),
-				IPV6RoutingEnabled:       copyBoolIfNotNil(ds.IPV6RoutingEnabled),
+				GlobalMaxMBPS:            util.CopyIfNotNil(ds.GlobalMaxMBPS),
+				GlobalMaxTPS:             util.CopyIfNotNil(ds.GlobalMaxTPS),
+				HTTPBypassFQDN:           util.CopyIfNotNil(ds.HTTPBypassFQDN),
+				ID:                       util.CopyIfNotNil(ds.ID),
+				InfoURL:                  util.CopyIfNotNil(ds.InfoURL),
+				InitialDispersion:        util.CopyIfNotNil(ds.InitialDispersion),
+				IPV6RoutingEnabled:       util.CopyIfNotNil(ds.IPV6RoutingEnabled),
 				LastUpdated:              TimeNoModFromTime(ds.LastUpdated),
 				LogsEnabled:              util.BoolPtr(ds.LogsEnabled),
 				LongDesc:                 util.StrPtr(ds.LongDesc),
-				MaxDNSAnswers:            copyIntIfNotNil(ds.MaxDNSAnswers),
-				MidHeaderRewrite:         copyStringIfNotNil(ds.MidHeaderRewrite),
-				MissLat:                  copyFloatIfNotNil(ds.MissLat),
-				MissLong:                 copyFloatIfNotNil(ds.MissLong),
+				MaxDNSAnswers:            util.CopyIfNotNil(ds.MaxDNSAnswers),
+				MidHeaderRewrite:         util.CopyIfNotNil(ds.MidHeaderRewrite),
+				MissLat:                  util.CopyIfNotNil(ds.MissLat),
+				MissLong:                 util.CopyIfNotNil(ds.MissLong),
 				MultiSiteOrigin:          util.BoolPtr(ds.MultiSiteOrigin),
-				OriginShield:             copyStringIfNotNil(ds.OriginShield),
-				OrgServerFQDN:            copyStringIfNotNil(ds.OrgServerFQDN),
-				ProfileDesc:              copyStringIfNotNil(ds.ProfileDesc),
-				ProfileID:                copyIntIfNotNil(ds.ProfileID),
-				ProfileName:              copyStringIfNotNil(ds.ProfileName),
-				Protocol:                 copyIntIfNotNil(ds.Protocol),
-				QStringIgnore:            copyIntIfNotNil(ds.QStringIgnore),
-				RangeRequestHandling:     copyIntIfNotNil(ds.RangeRequestHandling),
-				RegexRemap:               copyStringIfNotNil(ds.RegexRemap),
+				OriginShield:             util.CopyIfNotNil(ds.OriginShield),
+				OrgServerFQDN:            util.CopyIfNotNil(ds.OrgServerFQDN),
+				ProfileDesc:              util.CopyIfNotNil(ds.ProfileDesc),
+				ProfileID:                util.CopyIfNotNil(ds.ProfileID),
+				ProfileName:              util.CopyIfNotNil(ds.ProfileName),
+				Protocol:                 util.CopyIfNotNil(ds.Protocol),
+				QStringIgnore:            util.CopyIfNotNil(ds.QStringIgnore),
+				RangeRequestHandling:     util.CopyIfNotNil(ds.RangeRequestHandling),
+				RegexRemap:               util.CopyIfNotNil(ds.RegexRemap),
 				RegionalGeoBlocking:      util.BoolPtr(ds.RegionalGeoBlocking),
-				RemapText:                copyStringIfNotNil(ds.RemapText),
+				RemapText:                util.CopyIfNotNil(ds.RemapText),
 				RoutingName:              util.StrPtr(ds.RoutingName),
 				Signed:                   ds.Signed,
-				SSLKeyVersion:            copyIntIfNotNil(ds.SSLKeyVersion),
+				SSLKeyVersion:            util.CopyIfNotNil(ds.SSLKeyVersion),
 				TenantID:                 util.IntPtr(ds.TenantID),
-				Type:                     (*DSType)(copyStringIfNotNil(ds.Type)),
+				Type:                     (*DSType)(util.CopyIfNotNil(ds.Type)),
 				TypeID:                   util.IntPtr(ds.TypeID),
 				XMLID:                    util.StrPtr(ds.XMLID),
 			},
@@ -1642,76 +1642,76 @@ func (ds DeliveryServiceV5) Downgrade() DeliveryServiceV4 {
 // the latest minor version.
 func (ds DeliveryServiceV4) Upgrade() DeliveryServiceV5 {
 	upgraded := DeliveryServiceV5{
-		AnonymousBlockingEnabled:  coalesceBool(ds.AnonymousBlockingEnabled, false),
-		CCRDNSTTL:                 copyIntIfNotNil(ds.CCRDNSTTL),
-		CDNID:                     coalesceInt(ds.CDNID, -1),
-		CDNName:                   copyStringIfNotNil(ds.CDNName),
-		CheckPath:                 copyStringIfNotNil(ds.CheckPath),
+		AnonymousBlockingEnabled:  util.CoalesceToDefault(ds.AnonymousBlockingEnabled),
+		CCRDNSTTL:                 util.CopyIfNotNil(ds.CCRDNSTTL),
+		CDNID:                     util.Coalesce(ds.CDNID, -1),
+		CDNName:                   util.CopyIfNotNil(ds.CDNName),
+		CheckPath:                 util.CopyIfNotNil(ds.CheckPath),
 		ConsistentHashQueryParams: make([]string, len(ds.ConsistentHashQueryParams)),
-		ConsistentHashRegex:       copyStringIfNotNil(ds.ConsistentHashRegex),
-		DeepCachingType:           DeepCachingType(coalesceString((*string)(ds.DeepCachingType), "")),
-		DisplayName:               coalesceString(ds.DisplayName, ""),
-		DNSBypassCNAME:            copyStringIfNotNil(ds.DNSBypassCNAME),
-		DNSBypassIP:               copyStringIfNotNil(ds.DNSBypassIP),
-		DNSBypassIP6:              copyStringIfNotNil(ds.DNSBypassIP6),
-		DNSBypassTTL:              copyIntIfNotNil(ds.DNSBypassTTL),
-		DSCP:                      coalesceInt(ds.DSCP, -1),
+		ConsistentHashRegex:       util.CopyIfNotNil(ds.ConsistentHashRegex),
+		DeepCachingType:           util.CoalesceToDefault(ds.DeepCachingType),
+		DisplayName:               util.CoalesceToDefault(ds.DisplayName),
+		DNSBypassCNAME:            util.CopyIfNotNil(ds.DNSBypassCNAME),
+		DNSBypassIP:               util.CopyIfNotNil(ds.DNSBypassIP),
+		DNSBypassIP6:              util.CopyIfNotNil(ds.DNSBypassIP6),
+		DNSBypassTTL:              util.CopyIfNotNil(ds.DNSBypassTTL),
+		DSCP:                      util.Coalesce(ds.DSCP, -1),
 		EcsEnabled:                ds.EcsEnabled,
-		EdgeHeaderRewrite:         copyStringIfNotNil(ds.EdgeHeaderRewrite),
+		EdgeHeaderRewrite:         util.CopyIfNotNil(ds.EdgeHeaderRewrite),
 		ExampleURLs:               nil,
-		FirstHeaderRewrite:        copyStringIfNotNil(ds.FirstHeaderRewrite),
-		FQPacingRate:              copyIntIfNotNil(ds.FQPacingRate),
-		GeoLimit:                  coalesceInt(ds.GeoLimit, -1),
+		FirstHeaderRewrite:        util.CopyIfNotNil(ds.FirstHeaderRewrite),
+		FQPacingRate:              util.CopyIfNotNil(ds.FQPacingRate),
+		GeoLimit:                  util.Coalesce(ds.GeoLimit, -1),
 		GeoLimitCountries:         make([]string, len(ds.GeoLimitCountries)),
-		GeoLimitRedirectURL:       copyStringIfNotNil(ds.GeoLimitRedirectURL),
-		GeoProvider:               coalesceInt(ds.GeoProvider, -1),
-		GlobalMaxMBPS:             copyIntIfNotNil(ds.GlobalMaxMBPS),
-		GlobalMaxTPS:              copyIntIfNotNil(ds.GlobalMaxTPS),
-		HTTPBypassFQDN:            copyStringIfNotNil(ds.HTTPBypassFQDN),
-		ID:                        copyIntIfNotNil(ds.ID),
-		InfoURL:                   copyStringIfNotNil(ds.InfoURL),
-		InitialDispersion:         copyIntIfNotNil(ds.InitialDispersion),
-		InnerHeaderRewrite:        copyStringIfNotNil(ds.InnerHeaderRewrite),
-		IPV6RoutingEnabled:        copyBoolIfNotNil(ds.IPV6RoutingEnabled),
-		LastHeaderRewrite:         copyStringIfNotNil(ds.LastHeaderRewrite),
-		LogsEnabled:               coalesceBool(ds.LogsEnabled, false),
-		LongDesc:                  coalesceString(ds.LongDesc, ""),
+		GeoLimitRedirectURL:       util.CopyIfNotNil(ds.GeoLimitRedirectURL),
+		GeoProvider:               util.Coalesce(ds.GeoProvider, -1),
+		GlobalMaxMBPS:             util.CopyIfNotNil(ds.GlobalMaxMBPS),
+		GlobalMaxTPS:              util.CopyIfNotNil(ds.GlobalMaxTPS),
+		HTTPBypassFQDN:            util.CopyIfNotNil(ds.HTTPBypassFQDN),
+		ID:                        util.CopyIfNotNil(ds.ID),
+		InfoURL:                   util.CopyIfNotNil(ds.InfoURL),
+		InitialDispersion:         util.CopyIfNotNil(ds.InitialDispersion),
+		InnerHeaderRewrite:        util.CopyIfNotNil(ds.InnerHeaderRewrite),
+		IPV6RoutingEnabled:        util.CopyIfNotNil(ds.IPV6RoutingEnabled),
+		LastHeaderRewrite:         util.CopyIfNotNil(ds.LastHeaderRewrite),
+		LogsEnabled:               util.Coalesce(ds.LogsEnabled, false),
+		LongDesc:                  util.CoalesceToDefault(ds.LongDesc),
 		MatchList:                 nil,
-		MaxDNSAnswers:             copyIntIfNotNil(ds.MaxDNSAnswers),
-		MaxOriginConnections:      copyIntIfNotNil(ds.MaxOriginConnections),
-		MaxRequestHeaderBytes:     copyIntIfNotNil(ds.MaxRequestHeaderBytes),
-		MidHeaderRewrite:          copyStringIfNotNil(ds.MidHeaderRewrite),
-		MissLat:                   copyFloatIfNotNil(ds.MissLat),
-		MissLong:                  copyFloatIfNotNil(ds.MissLong),
-		MultiSiteOrigin:           coalesceBool(ds.MultiSiteOrigin, false),
-		OriginShield:              copyStringIfNotNil(ds.OriginShield),
-		OrgServerFQDN:             copyStringIfNotNil(ds.OrgServerFQDN),
-		ProfileDesc:               copyStringIfNotNil(ds.ProfileDesc),
-		ProfileID:                 copyIntIfNotNil(ds.ProfileID),
-		ProfileName:               copyStringIfNotNil(ds.ProfileName),
-		Protocol:                  copyIntIfNotNil(ds.Protocol),
-		QStringIgnore:             copyIntIfNotNil(ds.QStringIgnore),
-		RangeRequestHandling:      copyIntIfNotNil(ds.RangeRequestHandling),
-		RangeSliceBlockSize:       copyIntIfNotNil(ds.RangeSliceBlockSize),
-		RegexRemap:                copyStringIfNotNil(ds.RegexRemap),
+		MaxDNSAnswers:             util.CopyIfNotNil(ds.MaxDNSAnswers),
+		MaxOriginConnections:      util.CopyIfNotNil(ds.MaxOriginConnections),
+		MaxRequestHeaderBytes:     util.CopyIfNotNil(ds.MaxRequestHeaderBytes),
+		MidHeaderRewrite:          util.CopyIfNotNil(ds.MidHeaderRewrite),
+		MissLat:                   util.CopyIfNotNil(ds.MissLat),
+		MissLong:                  util.CopyIfNotNil(ds.MissLong),
+		MultiSiteOrigin:           util.CoalesceToDefault(ds.MultiSiteOrigin),
+		OriginShield:              util.CopyIfNotNil(ds.OriginShield),
+		OrgServerFQDN:             util.CopyIfNotNil(ds.OrgServerFQDN),
+		ProfileDesc:               util.CopyIfNotNil(ds.ProfileDesc),
+		ProfileID:                 util.CopyIfNotNil(ds.ProfileID),
+		ProfileName:               util.CopyIfNotNil(ds.ProfileName),
+		Protocol:                  util.CopyIfNotNil(ds.Protocol),
+		QStringIgnore:             util.CopyIfNotNil(ds.QStringIgnore),
+		RangeRequestHandling:      util.CopyIfNotNil(ds.RangeRequestHandling),
+		RangeSliceBlockSize:       util.CopyIfNotNil(ds.RangeSliceBlockSize),
+		RegexRemap:                util.CopyIfNotNil(ds.RegexRemap),
 		Regional:                  ds.Regional,
-		RegionalGeoBlocking:       coalesceBool(ds.RegionalGeoBlocking, false),
-		RemapText:                 copyStringIfNotNil(ds.RemapText),
+		RegionalGeoBlocking:       util.CoalesceToDefault(ds.RegionalGeoBlocking),
+		RemapText:                 util.CopyIfNotNil(ds.RemapText),
 		RequiredCapabilities:      make([]string, len(ds.RequiredCapabilities)),
-		RoutingName:               coalesceString(ds.RoutingName, ""),
-		ServiceCategory:           copyStringIfNotNil(ds.ServiceCategory),
+		RoutingName:               util.CoalesceToDefault(ds.RoutingName),
+		ServiceCategory:           util.CopyIfNotNil(ds.ServiceCategory),
 		Signed:                    ds.Signed,
-		SigningAlgorithm:          copyStringIfNotNil(ds.SigningAlgorithm),
-		SSLKeyVersion:             copyIntIfNotNil(ds.SSLKeyVersion),
-		Tenant:                    copyStringIfNotNil(ds.Tenant),
-		TenantID:                  coalesceInt(ds.TenantID, -1),
+		SigningAlgorithm:          util.CopyIfNotNil(ds.SigningAlgorithm),
+		SSLKeyVersion:             util.CopyIfNotNil(ds.SSLKeyVersion),
+		Tenant:                    util.CopyIfNotNil(ds.Tenant),
+		TenantID:                  util.Coalesce(ds.TenantID, -1),
 		TLSVersions:               make([]string, len(ds.TLSVersions)),
-		Topology:                  copyStringIfNotNil(ds.Topology),
-		TRResponseHeaders:         copyStringIfNotNil(ds.TRResponseHeaders),
-		TRRequestHeaders:          copyStringIfNotNil(ds.TRRequestHeaders),
-		Type:                      copyStringIfNotNil((*string)(ds.Type)),
-		TypeID:                    coalesceInt(ds.TypeID, -1),
-		XMLID:                     coalesceString(ds.XMLID, ""),
+		Topology:                  util.CopyIfNotNil(ds.Topology),
+		TRResponseHeaders:         util.CopyIfNotNil(ds.TRResponseHeaders),
+		TRRequestHeaders:          util.CopyIfNotNil(ds.TRRequestHeaders),
+		Type:                      (*string)(util.CopyIfNotNil(ds.Type)),
+		TypeID:                    util.Coalesce(ds.TypeID, -1),
+		XMLID:                     util.CoalesceToDefault(ds.XMLID),
 	}
 
 	if ds.Active == nil || !*ds.Active {
diff --git a/lib/go-tc/users.go b/lib/go-tc/users.go
index 18e997f090..e06179fa23 100644
--- a/lib/go-tc/users.go
+++ b/lib/go-tc/users.go
@@ -36,23 +36,23 @@ import (
 // Upgrade converts a User to a UserV4 (as seen in API versions 4.x).
 func (u User) Upgrade() UserV4 {
 	var ret UserV4
-	ret.AddressLine1 = copyStringIfNotNil(u.AddressLine1)
-	ret.AddressLine2 = copyStringIfNotNil(u.AddressLine2)
-	ret.City = copyStringIfNotNil(u.City)
-	ret.Company = copyStringIfNotNil(u.Company)
-	ret.Country = copyStringIfNotNil(u.Country)
-	ret.Email = copyStringIfNotNil(u.Email)
-	ret.GID = copyIntIfNotNil(u.GID)
-	ret.ID = copyIntIfNotNil(u.ID)
-	ret.LocalPassword = copyStringIfNotNil(u.LocalPassword)
-	ret.PhoneNumber = copyStringIfNotNil(u.PhoneNumber)
-	ret.PostalCode = copyStringIfNotNil(u.PostalCode)
-	ret.PublicSSHKey = copyStringIfNotNil(u.PublicSSHKey)
-	ret.StateOrProvince = copyStringIfNotNil(u.StateOrProvince)
-	ret.Tenant = copyStringIfNotNil(u.Tenant)
-	ret.Token = copyStringIfNotNil(u.Token)
-	ret.UID = copyIntIfNotNil(u.UID)
-	ret.FullName = copyStringIfNotNil(u.FullName)
+	ret.AddressLine1 = util.CopyIfNotNil(u.AddressLine1)
+	ret.AddressLine2 = util.CopyIfNotNil(u.AddressLine2)
+	ret.City = util.CopyIfNotNil(u.City)
+	ret.Company = util.CopyIfNotNil(u.Company)
+	ret.Country = util.CopyIfNotNil(u.Country)
+	ret.Email = util.CopyIfNotNil(u.Email)
+	ret.GID = util.CopyIfNotNil(u.GID)
+	ret.ID = util.CopyIfNotNil(u.ID)
+	ret.LocalPassword = util.CopyIfNotNil(u.LocalPassword)
+	ret.PhoneNumber = util.CopyIfNotNil(u.PhoneNumber)
+	ret.PostalCode = util.CopyIfNotNil(u.PostalCode)
+	ret.PublicSSHKey = util.CopyIfNotNil(u.PublicSSHKey)
+	ret.StateOrProvince = util.CopyIfNotNil(u.StateOrProvince)
+	ret.Tenant = util.CopyIfNotNil(u.Tenant)
+	ret.Token = util.CopyIfNotNil(u.Token)
+	ret.UID = util.CopyIfNotNil(u.UID)
+	ret.FullName = util.CopyIfNotNil(u.FullName)
 	if u.LastUpdated != nil {
 		ret.LastUpdated = u.LastUpdated.Time
 	}
@@ -91,26 +91,26 @@ func (u UserV4) Downgrade() User {
 	ret.Username = new(string)
 	*ret.Username = u.Username
 
-	ret.AddressLine1 = copyStringIfNotNil(u.AddressLine1)
-	ret.AddressLine2 = copyStringIfNotNil(u.AddressLine2)
-	ret.City = copyStringIfNotNil(u.City)
-	ret.Company = copyStringIfNotNil(u.Company)
-	ret.ConfirmLocalPassword = copyStringIfNotNil(u.LocalPassword)
-	ret.Country = copyStringIfNotNil(u.Country)
-	ret.Email = copyStringIfNotNil(u.Email)
-	ret.GID = copyIntIfNotNil(u.GID)
-	ret.ID = copyIntIfNotNil(u.ID)
-	ret.LocalPassword = copyStringIfNotNil(u.LocalPassword)
-	ret.PhoneNumber = copyStringIfNotNil(u.PhoneNumber)
-	ret.PostalCode = copyStringIfNotNil(u.PostalCode)
-	ret.PublicSSHKey = copyStringIfNotNil(u.PublicSSHKey)
+	ret.AddressLine1 = util.CopyIfNotNil(u.AddressLine1)
+	ret.AddressLine2 = util.CopyIfNotNil(u.AddressLine2)
+	ret.City = util.CopyIfNotNil(u.City)
+	ret.Company = util.CopyIfNotNil(u.Company)
+	ret.ConfirmLocalPassword = util.CopyIfNotNil(u.LocalPassword)
+	ret.Country = util.CopyIfNotNil(u.Country)
+	ret.Email = util.CopyIfNotNil(u.Email)
+	ret.GID = util.CopyIfNotNil(u.GID)
+	ret.ID = util.CopyIfNotNil(u.ID)
+	ret.LocalPassword = util.CopyIfNotNil(u.LocalPassword)
+	ret.PhoneNumber = util.CopyIfNotNil(u.PhoneNumber)
+	ret.PostalCode = util.CopyIfNotNil(u.PostalCode)
+	ret.PublicSSHKey = util.CopyIfNotNil(u.PublicSSHKey)
 	if u.RegistrationSent != nil {
 		ret.RegistrationSent = TimeNoModFromTime(*u.RegistrationSent)
 	}
-	ret.StateOrProvince = copyStringIfNotNil(u.StateOrProvince)
-	ret.Tenant = copyStringIfNotNil(u.Tenant)
-	ret.Token = copyStringIfNotNil(u.Token)
-	ret.UID = copyIntIfNotNil(u.UID)
+	ret.StateOrProvince = util.CopyIfNotNil(u.StateOrProvince)
+	ret.Tenant = util.CopyIfNotNil(u.Tenant)
+	ret.Token = util.CopyIfNotNil(u.Token)
+	ret.UID = util.CopyIfNotNil(u.UID)
 
 	return ret
 }
@@ -194,23 +194,23 @@ func (u UserV4) ToLegacyCurrentUser(roleID int, localUser bool) UserCurrent {
 	*ret.UserName = u.Username
 	ret.LocalUser = new(bool)
 	*ret.LocalUser = localUser
-	ret.Token = copyStringIfNotNil(u.Token)
-	ret.AddressLine1 = copyStringIfNotNil(u.AddressLine1)
-	ret.AddressLine2 = copyStringIfNotNil(u.AddressLine2)
-	ret.City = copyStringIfNotNil(u.City)
-	ret.Company = copyStringIfNotNil(u.Company)
-	ret.Country = copyStringIfNotNil(u.Country)
-	ret.Email = copyStringIfNotNil(u.Email)
-	ret.GID = copyIntIfNotNil(u.GID)
-	ret.ID = copyIntIfNotNil(u.ID)
-	ret.UID = copyIntIfNotNil(u.UID)
-	ret.PhoneNumber = copyStringIfNotNil(u.PhoneNumber)
-	ret.PostalCode = copyStringIfNotNil(u.PostalCode)
-	ret.PublicSSHKey = copyStringIfNotNil(u.PublicSSHKey)
-	ret.StateOrProvince = copyStringIfNotNil(u.StateOrProvince)
-	ret.Tenant = copyStringIfNotNil(u.Tenant)
-	ret.Token = copyStringIfNotNil(u.Token)
-	ret.UID = copyIntIfNotNil(u.UID)
+	ret.Token = util.CopyIfNotNil(u.Token)
+	ret.AddressLine1 = util.CopyIfNotNil(u.AddressLine1)
+	ret.AddressLine2 = util.CopyIfNotNil(u.AddressLine2)
+	ret.City = util.CopyIfNotNil(u.City)
+	ret.Company = util.CopyIfNotNil(u.Company)
+	ret.Country = util.CopyIfNotNil(u.Country)
+	ret.Email = util.CopyIfNotNil(u.Email)
+	ret.GID = util.CopyIfNotNil(u.GID)
+	ret.ID = util.CopyIfNotNil(u.ID)
+	ret.UID = util.CopyIfNotNil(u.UID)
+	ret.PhoneNumber = util.CopyIfNotNil(u.PhoneNumber)
+	ret.PostalCode = util.CopyIfNotNil(u.PostalCode)
+	ret.PublicSSHKey = util.CopyIfNotNil(u.PublicSSHKey)
+	ret.StateOrProvince = util.CopyIfNotNil(u.StateOrProvince)
+	ret.Tenant = util.CopyIfNotNil(u.Tenant)
+	ret.Token = util.CopyIfNotNil(u.Token)
+	ret.UID = util.CopyIfNotNil(u.UID)
 
 	return ret
 }
@@ -302,25 +302,25 @@ type CurrentUserUpdateRequestUser struct {
 // Fields not present in earlier API versions need to be passed explicitly
 func (u UserCurrent) Upgrade(registrationSent, lastAuthenticated *time.Time, ucdn string, changeLogCount int) UserV4 {
 	var ret UserV4
-	ret.AddressLine1 = copyStringIfNotNil(u.AddressLine1)
-	ret.AddressLine2 = copyStringIfNotNil(u.AddressLine2)
+	ret.AddressLine1 = util.CopyIfNotNil(u.AddressLine1)
+	ret.AddressLine2 = util.CopyIfNotNil(u.AddressLine2)
 	ret.ChangeLogCount = changeLogCount
-	ret.City = copyStringIfNotNil(u.City)
-	ret.Company = copyStringIfNotNil(u.Company)
-	ret.Country = copyStringIfNotNil(u.Country)
-	ret.Email = copyStringIfNotNil(u.Email)
-	ret.GID = copyIntIfNotNil(u.GID)
-	ret.ID = copyIntIfNotNil(u.ID)
+	ret.City = util.CopyIfNotNil(u.City)
+	ret.Company = util.CopyIfNotNil(u.Company)
+	ret.Country = util.CopyIfNotNil(u.Country)
+	ret.Email = util.CopyIfNotNil(u.Email)
+	ret.GID = util.CopyIfNotNil(u.GID)
+	ret.ID = util.CopyIfNotNil(u.ID)
 	ret.LastAuthenticated = lastAuthenticated
-	ret.PhoneNumber = copyStringIfNotNil(u.PhoneNumber)
-	ret.PostalCode = copyStringIfNotNil(u.PostalCode)
-	ret.PublicSSHKey = copyStringIfNotNil(u.PublicSSHKey)
+	ret.PhoneNumber = util.CopyIfNotNil(u.PhoneNumber)
+	ret.PostalCode = util.CopyIfNotNil(u.PostalCode)
+	ret.PublicSSHKey = util.CopyIfNotNil(u.PublicSSHKey)
 	ret.RegistrationSent = registrationSent
-	ret.StateOrProvince = copyStringIfNotNil(u.StateOrProvince)
-	ret.Tenant = copyStringIfNotNil(u.Tenant)
-	ret.Token = copyStringIfNotNil(u.Token)
+	ret.StateOrProvince = util.CopyIfNotNil(u.StateOrProvince)
+	ret.Tenant = util.CopyIfNotNil(u.Tenant)
+	ret.Token = util.CopyIfNotNil(u.Token)
 	ret.UCDN = ucdn
-	ret.UID = copyIntIfNotNil(u.UID)
+	ret.UID = util.CopyIfNotNil(u.UID)
 	ret.FullName = u.FullName
 	if u.LastUpdated != nil {
 		ret.LastUpdated = u.LastUpdated.Time
diff --git a/lib/go-util/ptr.go b/lib/go-util/ptr.go
index 6b33346d41..2517a77a91 100644
--- a/lib/go-util/ptr.go
+++ b/lib/go-util/ptr.go
@@ -1,3 +1,7 @@
+// Package util contains various miscellaneous utilities that are helpful
+// throughout components and aren't necessarily related to ATC data structures.
+package util
+
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements.  See the NOTICE file
@@ -17,50 +21,129 @@
  * under the License.
  */
 
-package util
-
 import "time"
 
+// Ptr returns a pointer to the given value.
 func Ptr[T any](v T) *T {
 	return &v
 }
 
+// StrPtr returns a pointer to the given string.
+//
+// Deprecated. This is exactly equivalent to just using Ptr, so duplicated
+// functionality like this function will likely be removed before too long.
 func StrPtr(str string) *string {
-	return &str
+	return Ptr(str)
 }
 
+// IntPtr returns a pointer to the given integer.
+//
+// Deprecated. This is exactly equivalent to just using Ptr, so duplicated
+// functionality like this function will likely be removed before too long.
 func IntPtr(i int) *int {
-	return &i
+	return Ptr(i)
 }
 
+// UIntPtr returns a pointer to the given unsigned integer.
+//
+// Deprecated. This is exactly equivalent to just using Ptr, so duplicated
+// functionality like this function will likely be removed before too long.
 func UIntPtr(u uint) *uint {
-	return &u
+	return Ptr(u)
 }
 
+// UInt64Ptr returns a pointer to the given 64-bit unsigned integer.
+//
+// Deprecated. This is exactly equivalent to just using Ptr, so duplicated
+// functionality like this function will likely be removed before too long.
 func UInt64Ptr(u uint64) *uint64 {
-	return &u
+	return Ptr(u)
 }
 
+// Uint64Ptr returns a pointer to the given 64-bit unsigned integer.
+//
+// Deprecated. This is just a common mis-casing of UInt64Ptr. These should not
+// both exist, and this one - being the less proper casing - is subject to
+// removal without warning, as its very existence is likely accidental.
+//
+// Deprecated. This is exactly equivalent to just using Ptr, so duplicated
+// functionality like this function will likely be removed before too long.
 func Uint64Ptr(u uint64) *uint64 {
-	return &u
+	return Ptr(u)
 }
 
+// Int64Ptr returns a pointer to the given 64-bit integer.
+//
+// Deprecated. This is exactly equivalent to just using Ptr, so duplicated
+// functionality like this function will likely be removed before too long.
 func Int64Ptr(i int64) *int64 {
-	return &i
+	return Ptr(i)
 }
 
+// BoolPtr returns a pointer to the given boolean.
+//
+// Deprecated. This is exactly equivalent to just using Ptr, so duplicated
+// functionality like this function will likely be removed before too long.
 func BoolPtr(b bool) *bool {
-	return &b
+	return Ptr(b)
 }
 
+// FloatPtr returns a pointer to the given 64-bit floating-point number.
+//
+// Deprecated. This is exactly equivalent to just using Ptr, so duplicated
+// functionality like this function will likely be removed before too long.
 func FloatPtr(f float64) *float64 {
-	return &f
+	return Ptr(f)
 }
 
-func InterfacePtr(i interface{}) *interface{} {
-	return &i
+// InterfacePtr returns a pointer to the given empty interface.
+//
+// Deprecated. This is exactly equivalent to just using Ptr, so duplicated
+// functionality like this function will likely be removed before too long.
+func InterfacePtr(i any) *any {
+	return Ptr(i)
 }
 
+// TimePtr returns a pointer to the given time.Time value.
+//
+// Deprecated. This is exactly equivalent to just using Ptr, so duplicated
+// functionality like this function will likely be removed before too long.
 func TimePtr(t time.Time) *time.Time {
-	return &t
+	return Ptr(t)
+}
+
+// Coalesce coalesces the given pointer to a concrete value. This is basically
+// the inverse operation of Ptr - it safely dereferences its input. If the
+// pointer is nil, def is returned as a default value.
+func Coalesce[T any](p *T, def T) T {
+	if p == nil {
+		return def
+	}
+	return *p
+}
+
+// CoalesceToDefault coalesces a pointer to the type to which it points. It
+// returns the "zero value" of its input's pointed-to type when the input is
+// nil. This is equivalent to:
+//
+//	var x T
+//	result := CoalesceToDefault(p, x)
+//
+// ... but can be done on one line without knowing the type of `p`.
+func CoalesceToDefault[T any](p *T) T {
+	var ret T
+	if p != nil {
+		ret = *p
+	}
+	return ret
+}
+
+// CopyIfNotNil makes a deep copy of p - unless it's nil, in which case it just
+// returns nil.
+func CopyIfNotNil[T any](p *T) *T {
+	if p == nil {
+		return nil
+	}
+	q := *p
+	return &q
 }
diff --git a/lib/go-util/ptr_test.go b/lib/go-util/ptr_test.go
index 2cc769919d..70b1113099 100644
--- a/lib/go-util/ptr_test.go
+++ b/lib/go-util/ptr_test.go
@@ -19,7 +19,10 @@ package util
  * under the License.
  */
 
-import "fmt"
+import (
+	"fmt"
+	"time"
+)
 
 func ExamplePtr() {
 	ptr := Ptr("testquest")
@@ -80,3 +83,40 @@ func ExampleInterfacePtr() {
 	fmt.Println(*ptr)
 	// Output: (1+2i)
 }
+
+func ExampleTimePtr() {
+	ptr := TimePtr(time.Time{})
+	fmt.Println(*ptr)
+	// Output: 0001-01-01 00:00:00 +0000 UTC
+}
+
+func ExampleCopyIfNotNil() {
+	var i *int
+	fmt.Println(CopyIfNotNil(i))
+
+	s := Ptr("9000")
+	fmt.Println(*CopyIfNotNil(s))
+
+	// Output: <nil>
+	// 9000
+}
+
+func ExampleCoalesce() {
+	var i *int
+	fmt.Println(Coalesce(i, 9000))
+
+	s := Ptr("testquest")
+	fmt.Println(Coalesce(s, "9001"))
+	// Output: 9000
+	// testquest
+}
+
+func ExampleCoalesceToDefault() {
+	var i *int
+	fmt.Println(CoalesceToDefault(i))
+
+	s := Ptr("testquest")
+	fmt.Println(CoalesceToDefault(s))
+	// Output: 0
+	// testquest
+}
diff --git a/traffic_ops/testing/api/v3/deliveryservice_requests_test.go b/traffic_ops/testing/api/v3/deliveryservice_requests_test.go
index 2e43843fa9..fadf5e3fdc 100644
--- a/traffic_ops/testing/api/v3/deliveryservice_requests_test.go
+++ b/traffic_ops/testing/api/v3/deliveryservice_requests_test.go
@@ -114,7 +114,7 @@ func TestDeliveryServiceRequests(t *testing.T) {
 					},
 					Expectations: utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK)),
 				},
-				"BAD REQUEST when MISSING REQUIRED FIELDS": {
+				"BAD REQUEST when MISSING REQUIRED Delivery Service FIELDS": {
 					ClientSession: TOSession,
 					RequestBody: map[string]interface{}{
 						"changeType": "create",
@@ -126,6 +126,49 @@ func TestDeliveryServiceRequests(t *testing.T) {
 					},
 					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
 				},
+				"bad request when missing Delivery Service definition": {
+					ClientSession: TOSession,
+					RequestBody: map[string]any{
+						"changeType": "create",
+						"status":     "draft",
+					},
+				},
+				"bad request when missing status": {
+					ClientSession: TOSession,
+					RequestBody: map[string]interface{}{
+						"changeType": "create",
+						"deliveryService": generateDeliveryService(t, map[string]interface{}{
+							"ccrDnsTtl":          30,
+							"deepCachingType":    "NEVER",
+							"initialDispersion":  3,
+							"ipv6RoutingEnabled": true,
+							"longDesc":           "long desc",
+							"orgServerFqdn":      "http://example.test",
+							"profileName":        nil,
+							"tenant":             "root",
+							"xmlId":              "test-ds2",
+						}),
+					},
+					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
+				},
+				"bad request when missing change type": {
+					ClientSession: TOSession,
+					RequestBody: map[string]interface{}{
+						"deliveryService": generateDeliveryService(t, map[string]interface{}{
+							"ccrDnsTtl":          30,
+							"deepCachingType":    "NEVER",
+							"initialDispersion":  3,
+							"ipv6RoutingEnabled": true,
+							"longDesc":           "long desc",
+							"orgServerFqdn":      "http://example.test",
+							"profileName":        nil,
+							"tenant":             "root",
+							"xmlId":              "test-ds2",
+						}),
+						"status": "draft",
+					},
+					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
+				},
 				"BAD REQUEST when VALIDATION RULES ARE NOT FOLLOWED": {
 					ClientSession: TOSession,
 					RequestBody: map[string]interface{}{
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/request/requests.go b/traffic_ops/traffic_ops_golang/deliveryservice/request/requests.go
index 533625aab9..cc0e80a6b3 100644
--- a/traffic_ops/traffic_ops_golang/deliveryservice/request/requests.go
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/request/requests.go
@@ -620,7 +620,7 @@ func createLegacy(w http.ResponseWriter, r *http.Request, inf *api.APIInfo) (res
 		return
 	}
 
-	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Delivery Service request created", upgraded.Downgrade().Downgrade())
+	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Delivery Service request created", upgraded.Downgrade().Downgrade().Downgrade())
 
 	result.Successful = true
 	result.Assignee = dsr.Assignee
@@ -749,9 +749,13 @@ func Delete(w http.ResponseWriter, r *http.Request) {
 	case 5:
 		resp = dsr
 	case 4:
-		resp = dsr.Downgrade()
+		if inf.Version.Minor >= 1 {
+			resp = dsr.Downgrade()
+		} else {
+			resp = dsr.Downgrade().Downgrade()
+		}
 	case 3:
-		resp = dsr.Downgrade().Downgrade()
+		resp = dsr.Downgrade().Downgrade().Downgrade()
 	}
 
 	api.WriteRespAlertObj(w, r, tc.SuccessLevel, fmt.Sprintf("Delivery Service Request #%d deleted", inf.IntParams["id"]), resp)
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/request/validate.go b/traffic_ops/traffic_ops_golang/deliveryservice/request/validate.go
index 557baf3fbb..7b5883a3d8 100644
--- a/traffic_ops/traffic_ops_golang/deliveryservice/request/validate.go
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/request/validate.go
@@ -67,6 +67,9 @@ func validateLegacy(dsr tc.DeliveryServiceRequestNullable, tx *sql.Tx) (error, e
 		"status":          validation.Validate(dsr.Status, validation.Required, validation.By(validTransition)),
 	}
 	errs := tovalidate.ToErrors(errMap)
+	if len(errs) > 0 {
+		return util.JoinErrs(errs), nil
+	}
 	// ensure the deliveryservice requested is valid
 	upgraded := dsr.DeliveryService.UpgradeToV4().Upgrade()
 	userErr, sysErr := deliveryservice.Validate(tx, &upgraded)