Repository: prebid/prebid-server
Pull request #2360 (2022-08-24 16:46:25)

Remove support for yssp adapter


Patch:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index a1459626..8b4d73b3 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -9,7 +9,7 @@ jobs:
   release:
     name: Create Release
     if: github.event.base_ref == 'refs/heads/master'
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-20.04
     steps:
       - name: Get Version
         id: get_version
diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml
index 570114b0..b56ddad9 100644
--- a/.github/workflows/security.yml
+++ b/.github/workflows/security.yml
@@ -9,7 +9,7 @@ on:
 jobs:
   build:
     name: Trivy
-    runs-on: ubuntu-18.04
+    runs-on: ubuntu-20.04
     steps:
       - name: Checkout Code
         uses: actions/checkout@v2
diff --git a/.github/workflows/validate-merge.yml b/.github/workflows/validate-merge.yml
index 9cf371ca..63dd7a97 100644
--- a/.github/workflows/validate-merge.yml
+++ b/.github/workflows/validate-merge.yml
@@ -6,7 +6,7 @@ on:
 
 jobs:
   validate-merge:
-    runs-on: ubuntu-18.04
+    runs-on: ubuntu-20.04
     
     steps:
     - name: Install Go
diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
index fc36f45c..972d6c0e 100644
--- a/.github/workflows/validate.yml
+++ b/.github/workflows/validate.yml
@@ -11,7 +11,7 @@ jobs:
     strategy:
       matrix:
         go-version: [1.16.x, 1.17.x]
-        os: [ubuntu-18.04]
+        os: [ubuntu-20.04]
     runs-on: ${{ matrix.os }}
     
     steps:
diff --git a/Dockerfile b/Dockerfile
index 92cd74e7..c2214da5 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM ubuntu:18.04 AS build
+FROM ubuntu:20.04 AS build
 RUN apt-get update && \
     apt-get -y upgrade && \
     apt-get install -y wget
@@ -22,7 +22,7 @@ ARG TEST="true"
 RUN if [ "$TEST" != "false" ]; then ./validate.sh ; fi
 RUN go build -mod=vendor -ldflags "-X github.com/prebid/prebid-server/version.Ver=`git describe --tags | sed 's/^v//'` -X github.com/prebid/prebid-server/version.Rev=`git rev-parse HEAD`" .
 
-FROM ubuntu:18.04 AS release
+FROM ubuntu:20.04 AS release
 LABEL maintainer="hans.hjort@xandr.com" 
 WORKDIR /usr/local/bin/
 COPY --from=build /app/prebid-server .
diff --git a/adapters/nextmillennium/nextmillennium.go b/adapters/nextmillennium/nextmillennium.go
index 605da43f..30e7e67c 100644
--- a/adapters/nextmillennium/nextmillennium.go
+++ b/adapters/nextmillennium/nextmillennium.go
@@ -16,16 +16,14 @@ type adapter struct {
 	endpoint string
 }
 
-type NextMillenniumBidRequest struct {
-	ID   string `json:"id"`
-	Test uint8  `json:"test,omitempty"`
-	Ext  struct {
-		Prebid struct {
-			StoredRequest struct {
-				ID string `json:"id"`
-			} `json:"storedrequest"`
-		} `json:"prebid"`
-	} `json:"ext"`
+type nmExtPrebidStoredRequest struct {
+	ID string `json:"id"`
+}
+type nmExtPrebid struct {
+	StoredRequest nmExtPrebidStoredRequest `json:"storedrequest"`
+}
+type nextMillJsonExt struct {
+	Prebid nmExtPrebid `json:"prebid"`
 }
 
 //MakeRequests prepares request information for prebid-server core
@@ -98,12 +96,8 @@ func (adapter *adapter) buildAdapterRequest(prebidBidRequest *openrtb2.BidReques
 		Headers: headers}, nil
 }
 
-func createBidRequest(prebidBidRequest *openrtb2.BidRequest, params *openrtb_ext.ImpExtNextMillennium) *NextMillenniumBidRequest {
+func createBidRequest(prebidBidRequest *openrtb2.BidRequest, params *openrtb_ext.ImpExtNextMillennium) *openrtb2.BidRequest {
 	placementID := params.PlacementID
-	bidRequest := NextMillenniumBidRequest{
-		ID:   prebidBidRequest.ID,
-		Test: uint8(prebidBidRequest.Test),
-	}
 
 	if params.GroupID != "" {
 		domain := ""
@@ -126,8 +120,15 @@ func createBidRequest(prebidBidRequest *openrtb2.BidRequest, params *openrtb_ext
 
 		placementID = fmt.Sprintf("g%s;%s;%s", params.GroupID, size, domain)
 	}
-
-	bidRequest.Ext.Prebid.StoredRequest.ID = placementID
+	ext := nextMillJsonExt{}
+	ext.Prebid.StoredRequest.ID = placementID
+	jsonExt, err := json.Marshal(ext)
+	if err != nil {
+		return prebidBidRequest
+	}
+	bidRequest := *prebidBidRequest
+	bidRequest.Ext = jsonExt
+	bidRequest.Imp[0].Ext = jsonExt
 	return &bidRequest
 }
 
diff --git a/config/config.go b/config/config.go
index 1c500adc..a6d71ee0 100644
--- a/config/config.go
+++ b/config/config.go
@@ -1085,7 +1085,6 @@ func SetupViper(v *viper.Viper, filename string) {
 	v.SetDefault("adapters.yieldlab.endpoint", "https://ad.yieldlab.net/yp/")
 	v.SetDefault("adapters.yieldmo.endpoint", "https://ads.yieldmo.com/exchange/prebid-server")
 	v.SetDefault("adapters.yieldone.endpoint", "https://y.one.impact-ad.jp/hbs_imp")
-	v.SetDefault("adapters.yssp.disabled", true)
 	v.SetDefault("adapters.zeroclickfraud.endpoint", "http://{{.Host}}/openrtb2?sid={{.SourceId}}")
 
 	v.SetDefault("max_request_size", 1024*256)
diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go
index 43dc7a10..9935ea74 100644
--- a/endpoints/openrtb2/auction.go
+++ b/endpoints/openrtb2/auction.go
@@ -1403,11 +1403,12 @@ func validateRegs(req *openrtb_ext.RequestWrapper) error {
 	if err != nil {
 		return fmt.Errorf("request.regs.ext is invalid: %v", err)
 	}
-	regExt := regsExt.GetExt()
-	gdprJSON, hasGDPR := regExt["gdpr"]
-	if hasGDPR && (string(gdprJSON) != "0" && string(gdprJSON) != "1") {
-		return errors.New("request.regs.ext.gdpr must be either 0 or 1.")
+
+	gdpr := regsExt.GetGDPR()
+	if gdpr != nil && *gdpr != 0 && *gdpr != 1 {
+		return errors.New("request.regs.ext.gdpr must be either 0 or 1")
 	}
+
 	return nil
 }
 
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-gdpr-invalid.json b/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-gdpr-invalid.json
index 1847dc72..03e789ee 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-gdpr-invalid.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-gdpr-invalid.json
@@ -44,5 +44,5 @@
     }
   },
   "expectedReturnCode": 400,
-  "expectedErrorMessage": "Invalid request: request.regs.ext.gdpr must be either 0 or 1.\n"
+  "expectedErrorMessage": "Invalid request: request.regs.ext.gdpr must be either 0 or 1\n"
 }
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-gdpr-string.json b/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-gdpr-string.json
index 4a315911..dc15410a 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-gdpr-string.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-gdpr-string.json
@@ -44,5 +44,5 @@
     }
   },
   "expectedReturnCode": 400,
-  "expectedErrorMessage": "Invalid request: request.regs.ext.gdpr must be either 0 or 1.\n"
+  "expectedErrorMessage": "Invalid request: request.regs.ext is invalid: gdpr must be an integer\n"
 }
diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go
index 978b634c..1d34708d 100755
--- a/exchange/adapter_builders.go
+++ b/exchange/adapter_builders.go
@@ -313,7 +313,6 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder {
 		openrtb_ext.BidderYieldlab:          yieldlab.Builder,
 		openrtb_ext.BidderYieldmo:           yieldmo.Builder,
 		openrtb_ext.BidderYieldone:          yieldone.Builder,
-		openrtb_ext.BidderYSSP:              yahoossp.Builder,
 		openrtb_ext.BidderZeroClickFraud:    zeroclickfraud.Builder,
 	}
 }
diff --git a/exchange/adapter_util.go b/exchange/adapter_util.go
index e95978b6..6ecac1ea 100644
--- a/exchange/adapter_util.go
+++ b/exchange/adapter_util.go
@@ -81,6 +81,7 @@ func GetDisabledBiddersErrorMessages(infos config.BidderInfos) map[string]string
 		"lifestreet":   `Bidder "lifestreet" is no longer available in Prebid Server. Please update your configuration.`,
 		"adagio":       `Bidder "adagio" is no longer available in Prebid Server. Please update your configuration.`,
 		"somoaudience": `Bidder "somoaudience" is no longer available in Prebid Server. Please update your configuration.`,
+		"yssp":         `Bidder "yssp" is no longer available in Prebid Server. If you're looking to use the Yahoo SSP adapter, please rename it to "yahoossp" in your configuration.`,
 	}
 
 	for name, info := range infos {
diff --git a/metrics/prometheus/prometheus.go b/metrics/prometheus/prometheus.go
index 1a8dd9fd..df54df92 100644
--- a/metrics/prometheus/prometheus.go
+++ b/metrics/prometheus/prometheus.go
@@ -425,7 +425,13 @@ func NewMetrics(cfg config.PrometheusMetrics, disabledMetrics config.DisabledMet
 
 	metrics.Gatherer = reg
 
-	metricsPrefix := fmt.Sprintf("%s_%s_", cfg.Namespace, cfg.Subsystem)
+	metricsPrefix := ""
+	if len(cfg.Namespace) > 0 {
+		metricsPrefix += fmt.Sprintf("%s_", cfg.Namespace)
+	}
+	if len(cfg.Subsystem) > 0 {
+		metricsPrefix += fmt.Sprintf("%s_", cfg.Subsystem)
+	}
 
 	metrics.Registerer = prometheus.WrapRegistererWithPrefix(metricsPrefix, reg)
 	metrics.Registerer.MustRegister(promCollector.NewGoCollector())
diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go
index 4ddc21c8..a1795b32 100644
--- a/openrtb_ext/bidders.go
+++ b/openrtb_ext/bidders.go
@@ -238,7 +238,6 @@ const (
 	BidderYieldlab          BidderName = "yieldlab"
 	BidderYieldmo           BidderName = "yieldmo"
 	BidderYieldone          BidderName = "yieldone"
-	BidderYSSP              BidderName = "yssp"
 	BidderZeroClickFraud    BidderName = "zeroclickfraud"
 )
 
@@ -402,7 +401,6 @@ func CoreBidderNames() []BidderName {
 		BidderYieldlab,
 		BidderYieldmo,
 		BidderYieldone,
-		BidderYSSP,
 		BidderZeroClickFraud,
 	}
 }
diff --git a/openrtb_ext/convert_down.go b/openrtb_ext/convert_down.go
new file mode 100644
index 00000000..95f8a948
--- /dev/null
+++ b/openrtb_ext/convert_down.go
@@ -0,0 +1,147 @@
+package openrtb_ext
+
+func ConvertDownTo25(r *RequestWrapper) error {
+	// schain
+	if err := moveSupplyChainFrom26To25(r); err != nil {
+		return err
+	}
+
+	// gdpr
+	if err := moveGDPRFrom26To25(r); err != nil {
+		return err
+	}
+	if err := moveConsentFrom26To25(r); err != nil {
+		return err
+	}
+
+	// ccpa
+	if err := moveUSPrivacyFrom26To25(r); err != nil {
+		return err
+	}
+
+	// eid
+	if err := moveEIDFrom26To25(r); err != nil {
+		return err
+	}
+
+	// Do not remove new OpenRTB 2.6 fields. The spec specifies that bidders and exchanges
+	// must tolerate new or unexpected fields gracefully, either ignoring them or treating
+	// them as unknown.
+
+	return nil
+}
+
+// moveSupplyChainFrom26To25 modifies the request to move the OpenRTB 2.6 supply chain
+// object (req.source.schain) to the OpenRTB 2.5 location (req.source.ext.schain). If the
+// OpenRTB 2.5 location is already present it may be overwritten. The OpenRTB 2.5 location
+// is expected to be empty.
+func moveSupplyChainFrom26To25(r *RequestWrapper) error {
+	if r.Source == nil || r.Source.SChain == nil {
+		return nil
+	}
+
+	// read and clear 2.6 location
+	schain26 := r.Source.SChain
+	r.Source.SChain = nil
+
+	// move to 2.5 location
+	sourceExt, err := r.GetSourceExt()
+	if err != nil {
+		return err
+	}
+	sourceExt.SetSChain(schain26)
+
+	return nil
+}
+
+// moveGDPRFrom26To25 modifies the request to move the OpenRTB 2.6 GDPR signal
+// field (req.regs.gdpr) to the OpenRTB 2.5 location (req.regs.ext.gdpr). If the
+// OpenRTB 2.5 location is already present it may be overwritten. The OpenRTB 2.5
+// location is expected to be empty.
+func moveGDPRFrom26To25(r *RequestWrapper) error {
+	if r.Regs == nil || r.Regs.GDPR == nil {
+		return nil
+	}
+
+	// read and clear 2.6 location
+	gdpr26 := r.Regs.GDPR
+	r.Regs.GDPR = nil
+
+	// move to 2.5 location
+	regExt, err := r.GetRegExt()
+	if err != nil {
+		return err
+	}
+	regExt.SetGDPR(gdpr26)
+
+	return nil
+}
+
+// moveConsentFrom26To25 modifies the request to move the OpenRTB 2.6 GDPR consent
+// field (req.user.consent) to the OpenRTB 2.5 location (req.user.ext.consent). If
+// the OpenRTB 2.5 location is already present it may be overwritten. The OpenRTB 2.5
+// location is expected to be empty.
+func moveConsentFrom26To25(r *RequestWrapper) error {
+	if r.User == nil || len(r.User.Consent) == 0 {
+		return nil
+	}
+
+	// read and clear 2.6 location
+	consent26 := r.User.Consent
+	r.User.Consent = ""
+
+	// move to 2.5 location
+	userExt, err := r.GetUserExt()
+	if err != nil {
+		return err
+	}
+	userExt.SetConsent(&consent26)
+
+	return nil
+}
+
+// moveUSPrivacyFrom26To25 modifies the request to move the OpenRTB 2.6 US Privacy (CCPA)
+// consent string (req.regs.us_privacy) to the OpenRTB 2.5 location (req.regs.ext.us_privacy).
+// If the OpenRTB 2.5 location is already present it may be overwritten. The OpenRTB 2.5
+// location is expected to be empty.
+func moveUSPrivacyFrom26To25(r *RequestWrapper) error {
+	if r.Regs == nil || len(r.Regs.USPrivacy) == 0 {
+		return nil
+	}
+
+	// read and clear 2.6 location
+	usprivacy26 := r.Regs.USPrivacy
+	r.Regs.USPrivacy = ""
+
+	// move to 2.5 location
+	regExt, err := r.GetRegExt()
+	if err != nil {
+		return err
+	}
+	regExt.SetUSPrivacy(usprivacy26)
+
+	return nil
+}
+
+// moveEIDFrom26To25 modifies the request to move the OpenRTB 2.6 external identifiers
+// (req.user.eids) to the OpenRTB 2.5 location (req.user.ext.eids). If the OpenRTB 2.5
+// location is already present it may be overwritten. The OpenRTB 2.5 location is
+// expected to be empty.
+func moveEIDFrom26To25(r *RequestWrapper) error {
+	if r.User == nil || r.User.EIDs == nil {
+		return nil
+	}
+
+	// read and clear 2.6 location
+	eid26 := r.User.EIDs
+	r.User.EIDs = nil
+
+	// move to 2.5 location
+	userExt, err := r.GetUserExt()
+	if err != nil {
+		return err
+	}
+	userExt.SetEid(&eid26)
+
+	return nil
+}
diff --git a/openrtb_ext/convert_up.go b/openrtb_ext/convert_up.go
new file mode 100644
index 00000000..6ecf0266
--- /dev/null
+++ b/openrtb_ext/convert_up.go
@@ -0,0 +1,150 @@
+package openrtb_ext
+
+import (
+	"fmt"
+
+	"github.com/mxmCherry/openrtb/v16/openrtb2"
+)
+
+func ConvertUpTo26(r *RequestWrapper) error {
+	if err := convertUpEnsureExt(r); err != nil {
+		return err
+	}
+
+	// schain
+	moveSupplyChainFrom24To25(r)
+	moveSupplyChainFrom25To26(r)
+
+	// gdpr
+	moveGDPRFrom25To26(r)
+	moveConsentFrom25To26(r)
+
+	// ccpa
+	moveUSPrivacyFrom25To26(r)
+
+	// eid
+	moveEIDFrom25To26(r)
+
+	return nil
+}
+
+// convertUpEnsureExt gets all extension objects required for migration to verify there
+// are no access errors.
+func convertUpEnsureExt(r *RequestWrapper) error {
+	if _, err := r.GetRequestExt(); err != nil {
+		return fmt.Errorf("req.ext is invalid: %v", err)
+	}
+
+	if _, err := r.GetSourceExt(); err != nil {
+		return fmt.Errorf("req.source.ext is invalid: %v", err)
+	}
+
+	if _, err := r.GetRegExt(); err != nil {
+		return fmt.Errorf("req.regs.ext is invalid: %v", err)
+	}
+
+	if _, err := r.GetUserExt(); err != nil {
+		return fmt.Errorf("req.user.ext is invalid: %v", err)
+	}
+
+	return nil
+}
+
+// moveSupplyChainFrom24To25 modifies the request to move the OpenRTB 2.4 supply chain
+// object (req.ext.schain) to the OpenRTB 2.5 location (req.source.ext.schain). If the
+// OpenRTB 2.5 location is already present the OpenRTB 2.4 supply chain object is dropped.
+func moveSupplyChainFrom24To25(r *RequestWrapper) {
+	// read and clear 2.4 location
+	reqExt, _ := r.GetRequestExt()
+	schain24 := reqExt.GetSChain()
+	reqExt.SetSChain(nil)
+
+	// move to 2.5 location if not already present
+	sourceExt, _ := r.GetSourceExt()
+	if sourceExt.GetSChain() == nil {
+		sourceExt.SetSChain(schain24)
+	}
+}
+
+// moveSupplyChainFrom25To26 modifies the request to move the OpenRTB 2.5 supply chain
+// object (req.source.ext.schain) to the OpenRTB 2.6 location (req.source.schain). If the
+// OpenRTB 2.6 location is already present the OpenRTB 2.5 supply chain object is dropped.
+func moveSupplyChainFrom25To26(r *RequestWrapper) {
+	// read and clear 2.5 location
+	sourceExt, _ := r.GetSourceExt()
+	schain25 := sourceExt.GetSChain()
+	sourceExt.SetSChain(nil)
+
+	// move to 2.6 location if not already present
+	if schain25 != nil {
+		// source may be nil if moved indirectly from an OpenRTB 2.4 location, since the ext
+		// is not defined on the source object.
+		if r.Source == nil {
+			r.Source = &openrtb2.Source{}
+		}
+
+		if r.Source.SChain == nil {
+			r.Source.SChain = schain25
+		}
+	}
+}
+
+// moveGDPRFrom25To26 modifies the request to move the OpenRTB 2.5 GDPR signal field
+// (req.regs.ext.gdpr) to the OpenRTB 2.6 location (req.regs.gdpr). If the OpenRTB 2.6
+// location is already present the OpenRTB 2.5 GDPR signal is dropped.
+func moveGDPRFrom25To26(r *RequestWrapper) {
+	// read and clear 2.5 location
+	regsExt, _ := r.GetRegExt()
+	gdpr25 := regsExt.GetGDPR()
+	regsExt.SetGDPR(nil)
+
+	// move to 2.6 location
+	if gdpr25 != nil && r.Regs.GDPR == nil {
+		r.Regs.GDPR = gdpr25
+	}
+}
+
+// moveConsentFrom25To26 modifies the request to move the OpenRTB 2.5 GDPR consent field
+// (req.user.ext.consent) to the OpenRTB 2.6 location (req.user.consent). If the OpenRTB 2.6
+// location is already present the OpenRTB 2.5 GDPR consent is dropped.
+func moveConsentFrom25To26(r *RequestWrapper) {
+	// read and clear 2.5 location
+	userExt, _ := r.GetUserExt()
+	consent25 := userExt.GetConsent()
+	userExt.SetConsent(nil)
+
+	// move to 2.6 location
+	if consent25 != nil && r.User.Consent == "" {
+		r.User.Consent = *consent25
+	}
+}
+
+// moveUSPrivacyFrom25To26 modifies the request to move the OpenRTB 2.5 US Privacy (CCPA)
+// consent string (req.regs.ext.usprivacy) to the OpenRTB 2.6 location (req.regs.usprivacy).
+// If the OpenRTB 2.6 location is already present the OpenRTB 2.5 consent string is dropped.
+func moveUSPrivacyFrom25To26(r *RequestWrapper) {
+	// read and clear 2.5 location
+	regsExt, _ := r.GetRegExt()
+	usPrivacy25 := regsExt.GetUSPrivacy()
+	regsExt.SetUSPrivacy("")
+
+	// move to 2.6 location
+	if usPrivacy25 != "" && r.Regs.USPrivacy == "" {
+		r.Regs.USPrivacy = usPrivacy25
+	}
+}
+
+// moveEIDFrom25To26 modifies the request to move the OpenRTB 2.5 external identifiers
+// (req.user.ext.eids) to the OpenRTB 2.6 location (req.user.eids). If the OpenRTB 2.6
+// location is already present the OpenRTB 2.5 external identifiers is dropped.
+func moveEIDFrom25To26(r *RequestWrapper) {
+	// read and clear 2.5 location
+	userExt, _ := r.GetUserExt()
+	eid25 := userExt.GetEid()
+	userExt.SetEid(nil)
+
+	// move to 2.6 location
+	if eid25 != nil && r.User.EIDs == nil {
+		r.User.EIDs = *eid25
+	}
+}
diff --git a/openrtb_ext/request_wrapper.go b/openrtb_ext/request_wrapper.go
index 51ce753d..380d14d1 100644
--- a/openrtb_ext/request_wrapper.go
+++ b/openrtb_ext/request_wrapper.go
@@ -299,7 +299,9 @@ func (ue *UserExt) unmarshal(extJson json.RawMessage) error {
 	if len(ue.ext) != 0 || ue.Dirty() {
 		return nil
 	}
+
 	ue.ext = make(map[string]json.RawMessage)
+
 	if len(extJson) == 0 {
 		return nil
 	}
@@ -458,26 +460,34 @@ func (re *RequestExt) unmarshal(extJson json.RawMessage) error {
 	if len(re.ext) != 0 || re.Dirty() {
 		return nil
 	}
+
 	re.ext = make(map[string]json.RawMessage)
+
 	if len(extJson) == 0 {
 		return nil
 	}
-	err := json.Unmarshal(extJson, &re.ext)
-	if err != nil {
+
+	if err := json.Unmarshal(extJson, &re.ext); err != nil {
 		return err
 	}
+
 	prebidJson, hasPrebid := re.ext["prebid"]
 	if hasPrebid {
 		re.prebid = &ExtRequestPrebid{}
-		err = json.Unmarshal(prebidJson, re.prebid)
+		if err := json.Unmarshal(prebidJson, re.prebid); err != nil {
+			return err
+		}
 	}
+
 	schainJson, hasSChain := re.ext["schain"]
 	if hasSChain {
 		re.schain = &openrtb2.SupplyChain{}
-		err = json.Unmarshal(schainJson, re.schain)
+		if err := json.Unmarshal(schainJson, re.schain); err != nil {
+			return err
+		}
 	}
 
-	return err
+	return nil
 }
 
 func (re *RequestExt) marshal() (json.RawMessage, error) {
@@ -587,21 +597,26 @@ func (de *DeviceExt) unmarshal(extJson json.RawMessage) error {
 	if len(de.ext) != 0 || de.Dirty() {
 		return nil
 	}
+
 	de.ext = make(map[string]json.RawMessage)
+
 	if len(extJson) == 0 {
 		return nil
 	}
-	err := json.Unmarshal(extJson, &de.ext)
-	if err != nil {
+
+	if err := json.Unmarshal(extJson, &de.ext); err != nil {
 		return err
 	}
+
 	prebidJson, hasPrebid := de.ext["prebid"]
 	if hasPrebid {
 		de.prebid = &ExtDevicePrebid{}
-		err = json.Unmarshal(prebidJson, de.prebid)
+		if err := json.Unmarshal(prebidJson, de.prebid); err != nil {
+			return err
+		}
 	}
 
-	return err
+	return nil
 }
 
 func (de *DeviceExt) marshal() (json.RawMessage, error) {
@@ -674,21 +689,26 @@ func (ae *AppExt) unmarshal(extJson json.RawMessage) error {
 	if len(ae.ext) != 0 || ae.Dirty() {
 		return nil
 	}
+
 	ae.ext = make(map[string]json.RawMessage)
+
 	if len(extJson) == 0 {
 		return nil
 	}
-	err := json.Unmarshal(extJson, &ae.ext)
-	if err != nil {
+
+	if err := json.Unmarshal(extJson, &ae.ext); err != nil {
 		return err
 	}
+
 	prebidJson, hasPrebid := ae.ext["prebid"]
 	if hasPrebid {
 		ae.prebid = &ExtAppPrebid{}
-		err = json.Unmarshal(prebidJson, ae.prebid)
+		if err := json.Unmarshal(prebidJson, ae.prebid); err != nil {
+			return err
+		}
 	}
 
-	return err
+	return nil
 }
 
 func (ae *AppExt) marshal() (json.RawMessage, error) {
@@ -753,6 +773,8 @@ func (ae *AppExt) SetPrebid(prebid *ExtAppPrebid) {
 type RegExt struct {
 	ext            map[string]json.RawMessage
 	extDirty       bool
+	gdpr           *int8
+	gdprDirty      bool
 	usPrivacy      string
 	usPrivacyDirty bool
 }
@@ -761,23 +783,48 @@ func (re *RegExt) unmarshal(extJson json.RawMessage) error {
 	if len(re.ext) != 0 || re.Dirty() {
 		return nil
 	}
+
 	re.ext = make(map[string]json.RawMessage)
+
 	if len(extJson) == 0 {
 		return nil
 	}
-	err := json.Unmarshal(extJson, &re.ext)
-	if err != nil {
+
+	if err := json.Unmarshal(extJson, &re.ext); err != nil {
 		return err
 	}
+
+	gdprJson, hasGDPR := re.ext["gdpr"]
+	if hasGDPR {
+		if err := json.Unmarshal(gdprJson, &re.gdpr); err != nil {
+			return errors.New("gdpr must be an integer")
+		}
+	}
+
 	uspJson, hasUsp := re.ext["us_privacy"]
 	if hasUsp {
-		err = json.Unmarshal(uspJson, &re.usPrivacy)
+		if err := json.Unmarshal(uspJson, &re.usPrivacy); err != nil {
+			return err
+		}
 	}
 
-	return err
+	return nil
 }
 
 func (re *RegExt) marshal() (json.RawMessage, error) {
+	if re.gdprDirty {
+		if re.gdpr != nil {
+			rawjson, err := json.Marshal(re.gdpr)
+			if err != nil {
+				return nil, err
+			}
+			re.ext["gdpr"] = rawjson
+		} else {
+			delete(re.ext, "gdpr")
+		}
+		re.gdprDirty = false
+	}
+
 	if re.usPrivacyDirty {
 		if len(re.usPrivacy) > 0 {
 			rawjson, err := json.Marshal(re.usPrivacy)
@@ -799,7 +846,7 @@ func (re *RegExt) marshal() (json.RawMessage, error) {
 }
 
 func (re *RegExt) Dirty() bool {
-	return re.extDirty || re.usPrivacyDirty
+	return re.extDirty || re.gdprDirty || re.usPrivacyDirty
 }
 
 func (re *RegExt) GetExt() map[string]json.RawMessage {
@@ -815,6 +862,16 @@ func (re *RegExt) SetExt(ext map[string]json.RawMessage) {
 	re.extDirty = true
 }
 
+func (re *RegExt) GetGDPR() *int8 {
+	gdpr := re.gdpr
+	return gdpr
+}
+
+func (re *RegExt) SetGDPR(gdpr *int8) {
+	re.gdpr = gdpr
+	re.gdprDirty = true
+}
+
 func (re *RegExt) GetUSPrivacy() string {
 	uSPrivacy := re.usPrivacy
 	return uSPrivacy
@@ -840,23 +897,25 @@ func (se *SiteExt) unmarshal(extJson json.RawMessage) error {
 	if len(se.ext) != 0 || se.Dirty() {
 		return nil
 	}
+
 	se.ext = make(map[string]json.RawMessage)
+
 	if len(extJson) == 0 {
 		return nil
 	}
-	err := json.Unmarshal(extJson, &se.ext)
-	if err != nil {
+
+	if err := json.Unmarshal(extJson, &se.ext); err != nil {
 		return err
 	}
-	AmpJson, hasAmp := se.ext["amp"]
+
+	ampJson, hasAmp := se.ext["amp"]
 	if hasAmp {
-		err = json.Unmarshal(AmpJson, &se.amp)
-		if err != nil {
-			err = errors.New(`request.site.ext.amp must be either 1, 0, or undefined`)
+		if err := json.Unmarshal(ampJson, &se.amp); err != nil {
+			return errors.New(`request.site.ext.amp must be either 1, 0, or undefined`)
 		}
 	}
 
-	return err
+	return nil
 }
 
 func (se *SiteExt) marshal() (json.RawMessage, error) {
@@ -921,20 +980,25 @@ func (se *SourceExt) unmarshal(extJson json.RawMessage) error {
 	if len(se.ext) != 0 || se.Dirty() {
 		return nil
 	}
+
 	se.ext = make(map[string]json.RawMessage)
+
 	if len(extJson) == 0 {
 		return nil
 	}
-	err := json.Unmarshal(extJson, &se.ext)
-	if err != nil {
+
+	if err := json.Unmarshal(extJson, &se.ext); err != nil {
 		return err
 	}
+
 	schainJson, hasSChain := se.ext["schain"]
 	if hasSChain {
-		err = json.Unmarshal(schainJson, &se.schain)
+		if err := json.Unmarshal(schainJson, &se.schain); err != nil {
+			return err
+		}
 	}
 
-	return err
+	return nil
 }
 
 func (se *SourceExt) marshal() (json.RawMessage, error) {
diff --git a/static/bidder-info/yssp.yaml b/static/bidder-info/yssp.yaml
deleted file mode 100644
index a1a20e0c..00000000
--- a/static/bidder-info/yssp.yaml
+++ /dev/null
@@ -1,15 +0,0 @@
-maintainer:
-  email: "hb-fe-tech@oath.com"
-gvlVendorID: 25
-capabilities:
-  app:
-    mediaTypes:
-      - banner
-  site:
-    mediaTypes:
-      - banner
-userSync:
-  # yssp supports user syncing, but requires configuration by the host. contact this
-  # bidder directly at the email address in this file to ask about enabling user sync.
-  supports:
-    - redirect
\ No newline at end of file
diff --git a/static/bidder-params/yssp.json b/static/bidder-params/yssp.json
deleted file mode 100644
index fdadc747..00000000
--- a/static/bidder-params/yssp.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
-	"$schema": "http://json-schema.org/draft-04/schema#",
-	"title": "YSSP Adapter Params",
-	"description": "A schema which validates params accepted by the YSSP adapter",
-	"type": "object",
-	"properties": {
-		"dcn": {
-			"type": "string",
-			"minLength": 1,
-			"description": "Site ID provided by One Mobile"
-		},
-		"pos": {
-			"type": "string",
-			"minLength": 1,
-			"description": "Placement ID"
-		}
-	},
-	"required": ["dcn", "pos"]
-}


Test patch:
diff --git a/adapters/nextmillennium/nextmillenniumtest/exemplary/banner-empty-group-id.json b/adapters/nextmillennium/nextmillenniumtest/exemplary/banner-empty-group-id.json
index dd9ee32d..7a628d0f 100644
--- a/adapters/nextmillennium/nextmillenniumtest/exemplary/banner-empty-group-id.json
+++ b/adapters/nextmillennium/nextmillenniumtest/exemplary/banner-empty-group-id.json
@@ -38,7 +38,31 @@
                 "id": "7819"
               }
             }
-          }
+          },
+          "imp": [
+            {
+              "banner": {
+                "format": [
+                  {
+                    "h": 250,
+                    "w": 320
+                  },
+                  {
+                    "h": 300,
+                    "w": 320
+                  }
+                ]
+              },
+              "ext": {
+                "prebid": {
+                  "storedrequest": {
+                    "id": "7819"
+                  }
+                }
+              },
+              "id": "testimpid"
+            }
+          ]
         }
       },
       "mockResponse": {
diff --git a/adapters/nextmillennium/nextmillenniumtest/exemplary/banner-with-group-id.json b/adapters/nextmillennium/nextmillenniumtest/exemplary/banner-with-group-id.json
index 72d22b28..35381130 100644
--- a/adapters/nextmillennium/nextmillenniumtest/exemplary/banner-with-group-id.json
+++ b/adapters/nextmillennium/nextmillenniumtest/exemplary/banner-with-group-id.json
@@ -49,6 +49,42 @@
                 "id": "g7819;320x250;www.example.com"
               }
             }
+          },
+          "imp": [
+            {
+              "banner": {
+                "format": [
+                  {
+                    "h": 250,
+                    "w": 320
+                  },
+                  {
+                    "h": 300,
+                    "w": 320
+                  }
+                ],
+                "h": 250,
+                "w": 320
+              },
+              "ext": {
+                "prebid": {
+                  "storedrequest": {
+                    "id": "g7819;320x250;www.example.com"
+                  }
+                }
+              },
+              "id": "testimpid"
+            }
+          ],
+          "site": {
+            "domain": "www.example.com",
+            "ext": {
+              "amp": 0
+            },
+            "page": "http://www.example.com",
+            "publisher": {
+              "domain": "example.com"
+            }
           }
         }
       },
diff --git a/adapters/nextmillennium/nextmillenniumtest/exemplary/banner-with-group-id_app.json b/adapters/nextmillennium/nextmillenniumtest/exemplary/banner-with-group-id_app.json
index 6633a063..25c1f36d 100644
--- a/adapters/nextmillennium/nextmillenniumtest/exemplary/banner-with-group-id_app.json
+++ b/adapters/nextmillennium/nextmillenniumtest/exemplary/banner-with-group-id_app.json
@@ -36,13 +36,42 @@
         "uri": "https://pbs.nextmillmedia.com/openrtb2/auction",
         "body":{
           "id": "testid",
+          "app": {
+            "domain": "www.example.com"
+          },
           "ext": {
             "prebid": {
               "storedrequest": {
                 "id": "g7819;320x250;www.example.com"
               }
             }
-          }
+          },
+          "imp": [
+            {
+              "banner": {
+                "format": [
+                  {
+                    "h": 250,
+                    "w": 320
+                  },
+                  {
+                    "h": 300,
+                    "w": 320
+                  }
+                ],
+                "h": 250,
+                "w": 320
+              },
+              "ext": {
+                "prebid": {
+                  "storedrequest": {
+                    "id": "g7819;320x250;www.example.com"
+                  }
+                }
+              },
+              "id": "testimpid"
+            }
+          ]
         }
       },
       "mockResponse": {
diff --git a/adapters/nextmillennium/nextmillenniumtest/exemplary/banner-with-only-width.json b/adapters/nextmillennium/nextmillenniumtest/exemplary/banner-with-only-width.json
index 2dc1f3b6..79c9894f 100644
--- a/adapters/nextmillennium/nextmillenniumtest/exemplary/banner-with-only-width.json
+++ b/adapters/nextmillennium/nextmillenniumtest/exemplary/banner-with-only-width.json
@@ -25,13 +25,31 @@
         "uri": "https://pbs.nextmillmedia.com/openrtb2/auction",
         "body":{
           "id": "testid",
+          "app": {
+            "domain": "www.example.com"
+          },
           "ext": {
             "prebid": {
               "storedrequest": {
                 "id": "g7819;;www.example.com"
               }
             }
-          }
+          },
+          "imp": [
+            {
+              "banner": {
+                "w": 320
+              },
+              "ext": {
+                "prebid": {
+                  "storedrequest": {
+                    "id": "g7819;;www.example.com"
+                  }
+                }
+              },
+              "id": "testimpid"
+            }
+          ]
         }
       },
       "mockResponse": {
diff --git a/adapters/nextmillennium/nextmillenniumtest/exemplary/banner-with-wh.json b/adapters/nextmillennium/nextmillenniumtest/exemplary/banner-with-wh.json
index 9e3385fd..6af7816e 100644
--- a/adapters/nextmillennium/nextmillenniumtest/exemplary/banner-with-wh.json
+++ b/adapters/nextmillennium/nextmillenniumtest/exemplary/banner-with-wh.json
@@ -26,13 +26,32 @@
         "uri": "https://pbs.nextmillmedia.com/openrtb2/auction",
         "body":{
           "id": "testid",
+          "app": {
+            "domain": "www.example.com"
+          },
           "ext": {
             "prebid": {
               "storedrequest": {
                 "id": "g7819;320x250;www.example.com"
               }
             }
-          }
+          },
+          "imp": [
+            {
+              "banner": {
+                "h": 250,
+                "w": 320
+              },
+              "ext": {
+                "prebid": {
+                  "storedrequest": {
+                    "id": "g7819;320x250;www.example.com"
+                  }
+                }
+              },
+              "id": "testimpid"
+            }
+          ]
         }
       },
       "mockResponse": {
diff --git a/adapters/nextmillennium/nextmillenniumtest/exemplary/banner-wo-domain.json b/adapters/nextmillennium/nextmillenniumtest/exemplary/banner-wo-domain.json
index 6a1694ba..181dab35 100644
--- a/adapters/nextmillennium/nextmillenniumtest/exemplary/banner-wo-domain.json
+++ b/adapters/nextmillennium/nextmillenniumtest/exemplary/banner-wo-domain.json
@@ -39,7 +39,33 @@
                 "id": "g7819;320x250;"
               }
             }
-          }
+          },
+          "imp": [
+            {
+              "banner": {
+                "format": [
+                  {
+                    "h": 250,
+                    "w": 320
+                  },
+                  {
+                    "h": 300,
+                    "w": 320
+                  }
+                ],
+                "h": 250,
+                "w": 320
+              },
+              "ext": {
+                "prebid": {
+                  "storedrequest": {
+                    "id": "g7819;320x250;"
+                  }
+                }
+              },
+              "id": "testimpid"
+            }
+          ]
         }
       },
       "mockResponse": {
diff --git a/adapters/nextmillennium/nextmillenniumtest/exemplary/banner-wo-size.json b/adapters/nextmillennium/nextmillenniumtest/exemplary/banner-wo-size.json
index 9da3ccf9..f2422033 100644
--- a/adapters/nextmillennium/nextmillenniumtest/exemplary/banner-wo-size.json
+++ b/adapters/nextmillennium/nextmillenniumtest/exemplary/banner-wo-size.json
@@ -37,7 +37,31 @@
                 "id": "g7819;320x250;"
               }
             }
-          }
+          },
+          "imp": [
+            {
+              "banner": {
+                "format": [
+                  {
+                    "h": 250,
+                    "w": 320
+                  },
+                  {
+                    "h": 300,
+                    "w": 320
+                  }
+                ]
+              },
+              "ext": {
+                "prebid": {
+                  "storedrequest": {
+                    "id": "g7819;320x250;"
+                  }
+                }
+              },
+              "id": "testimpid"
+            }
+          ]
         }
       },
       "mockResponse": {
diff --git a/adapters/nextmillennium/nextmillenniumtest/exemplary/banner.json b/adapters/nextmillennium/nextmillenniumtest/exemplary/banner.json
index ff6386d3..fa8834fa 100644
--- a/adapters/nextmillennium/nextmillenniumtest/exemplary/banner.json
+++ b/adapters/nextmillennium/nextmillenniumtest/exemplary/banner.json
@@ -39,7 +39,33 @@
                 "id": "7819"
               }
             }
-          }
+          },
+          "imp": [
+            {
+              "banner": {
+                "format": [
+                  {
+                    "h": 250,
+                    "w": 320
+                  },
+                  {
+                    "h": 300,
+                    "w": 320
+                  }
+                ],
+                "h": 250,
+                "w": 320
+              },
+              "ext": {
+                "prebid": {
+                  "storedrequest": {
+                    "id": "7819"
+                  }
+                }
+              },
+              "id": "testimpid"
+            }
+          ]
         }
       },
       "mockResponse": {
diff --git a/adapters/nextmillennium/nextmillenniumtest/exemplary/empty-banner-obj.json b/adapters/nextmillennium/nextmillenniumtest/exemplary/empty-banner-obj.json
index 52f53bdb..cc9f9eb9 100644
--- a/adapters/nextmillennium/nextmillenniumtest/exemplary/empty-banner-obj.json
+++ b/adapters/nextmillennium/nextmillenniumtest/exemplary/empty-banner-obj.json
@@ -26,7 +26,20 @@
                 "id": "g7819;;"
               }
             }
-          }
+          },
+          "imp": [
+            {
+              "banner": {},
+              "ext": {
+                "prebid": {
+                  "storedrequest": {
+                    "id": "g7819;;"
+                  }
+                }
+              },
+              "id": "testimpid"
+            }
+          ]
         }
       },
       "mockResponse": {
diff --git a/adapters/nextmillennium/nextmillenniumtest/supplemental/empty-seatbid.json b/adapters/nextmillennium/nextmillenniumtest/supplemental/empty-seatbid.json
index 8c344d57..dd070d62 100644
--- a/adapters/nextmillennium/nextmillenniumtest/supplemental/empty-seatbid.json
+++ b/adapters/nextmillennium/nextmillenniumtest/supplemental/empty-seatbid.json
@@ -38,7 +38,33 @@
                 "id": "7819"
               }
             }
-          }
+          },
+          "imp": [
+            {
+              "banner": {
+                "format": [
+                  {
+                    "h": 250,
+                    "w": 320
+                  },
+                  {
+                    "h": 300,
+                    "w": 320
+                  }
+                ],
+                "h": 250,
+                "w": 320
+              },
+              "ext": {
+                "prebid": {
+                  "storedrequest": {
+                    "id": "7819"
+                  }
+                }
+              },
+              "id": "testimpid"
+            }
+          ]
         }
       },
       "mockResponse": {
diff --git a/adapters/nextmillennium/nextmillenniumtest/supplemental/error-response.json b/adapters/nextmillennium/nextmillenniumtest/supplemental/error-response.json
index 1fa4b195..3e8047fa 100644
--- a/adapters/nextmillennium/nextmillenniumtest/supplemental/error-response.json
+++ b/adapters/nextmillennium/nextmillenniumtest/supplemental/error-response.json
@@ -34,7 +34,29 @@
                 "id": "7819"
               }
             }
-          }
+          },
+          "imp": [
+            {
+              "banner": {
+                "format": [
+                  {
+                    "h": 300,
+                    "w": 300
+                  }
+                ],
+                "h": 250,
+                "w": 320
+              },
+              "ext": {
+                "prebid": {
+                  "storedrequest": {
+                    "id": "7819"
+                  }
+                }
+              },
+              "id": "test-imp-id"
+            }
+          ]
         }
       },
       "mockResponse": {
diff --git a/adapters/nextmillennium/nextmillenniumtest/supplemental/no-content.json b/adapters/nextmillennium/nextmillenniumtest/supplemental/no-content.json
index a12897a4..c4710a37 100644
--- a/adapters/nextmillennium/nextmillenniumtest/supplemental/no-content.json
+++ b/adapters/nextmillennium/nextmillenniumtest/supplemental/no-content.json
@@ -34,7 +34,29 @@
                 "id": "7819"
               }
             }
-          }
+          },
+          "imp": [
+            {
+              "banner": {
+                "format": [
+                  {
+                    "h": 300,
+                    "w": 300
+                  }
+                ],
+                "h": 250,
+                "w": 320
+              },
+              "ext": {
+                "prebid": {
+                  "storedrequest": {
+                    "id": "7819"
+                  }
+                }
+              },
+              "id": "test-imp-id"
+            }
+          ]
         }
       },
       "mockResponse": {
diff --git a/exchange/adapter_util_test.go b/exchange/adapter_util_test.go
index 59b58f08..5266e67e 100644
--- a/exchange/adapter_util_test.go
+++ b/exchange/adapter_util_test.go
@@ -240,6 +240,7 @@ func TestGetDisabledBiddersErrorMessages(t *testing.T) {
 				"lifestreet":   `Bidder "lifestreet" is no longer available in Prebid Server. Please update your configuration.`,
 				"adagio":       `Bidder "adagio" is no longer available in Prebid Server. Please update your configuration.`,
 				"somoaudience": `Bidder "somoaudience" is no longer available in Prebid Server. Please update your configuration.`,
+				"yssp":         `Bidder "yssp" is no longer available in Prebid Server. If you're looking to use the Yahoo SSP adapter, please rename it to "yahoossp" in your configuration.`,
 			},
 		},
 		{
@@ -249,6 +250,7 @@ func TestGetDisabledBiddersErrorMessages(t *testing.T) {
 				"lifestreet":   `Bidder "lifestreet" is no longer available in Prebid Server. Please update your configuration.`,
 				"adagio":       `Bidder "adagio" is no longer available in Prebid Server. Please update your configuration.`,
 				"somoaudience": `Bidder "somoaudience" is no longer available in Prebid Server. Please update your configuration.`,
+				"yssp":         `Bidder "yssp" is no longer available in Prebid Server. If you're looking to use the Yahoo SSP adapter, please rename it to "yahoossp" in your configuration.`,
 			},
 		},
 		{
@@ -258,6 +260,7 @@ func TestGetDisabledBiddersErrorMessages(t *testing.T) {
 				"lifestreet":   `Bidder "lifestreet" is no longer available in Prebid Server. Please update your configuration.`,
 				"adagio":       `Bidder "adagio" is no longer available in Prebid Server. Please update your configuration.`,
 				"somoaudience": `Bidder "somoaudience" is no longer available in Prebid Server. Please update your configuration.`,
+				"yssp":         `Bidder "yssp" is no longer available in Prebid Server. If you're looking to use the Yahoo SSP adapter, please rename it to "yahoossp" in your configuration.`,
 				"appnexus":     `Bidder "appnexus" has been disabled on this instance of Prebid Server. Please work with the PBS host to enable this bidder again.`,
 			},
 		},
@@ -268,6 +271,7 @@ func TestGetDisabledBiddersErrorMessages(t *testing.T) {
 				"lifestreet":   `Bidder "lifestreet" is no longer available in Prebid Server. Please update your configuration.`,
 				"adagio":       `Bidder "adagio" is no longer available in Prebid Server. Please update your configuration.`,
 				"somoaudience": `Bidder "somoaudience" is no longer available in Prebid Server. Please update your configuration.`,
+				"yssp":         `Bidder "yssp" is no longer available in Prebid Server. If you're looking to use the Yahoo SSP adapter, please rename it to "yahoossp" in your configuration.`,
 				"appnexus":     `Bidder "appnexus" has been disabled on this instance of Prebid Server. Please work with the PBS host to enable this bidder again.`,
 			},
 		},
diff --git a/openrtb_ext/convert_down_test.go b/openrtb_ext/convert_down_test.go
new file mode 100644
index 00000000..2ffd2098
--- /dev/null
+++ b/openrtb_ext/convert_down_test.go
@@ -0,0 +1,354 @@
+package openrtb_ext
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/mxmCherry/openrtb/v16/openrtb2"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestConvertDownTo25(t *testing.T) {
+	testCases := []struct {
+		description     string
+		givenRequest    openrtb2.BidRequest
+		expectedRequest openrtb2.BidRequest
+		expectedErr     string
+	}{
+		{
+			description: "2.6 -> 2.5",
+			givenRequest: openrtb2.BidRequest{
+				ID:     "anyID",
+				Source: &openrtb2.Source{SChain: &openrtb2.SupplyChain{Complete: 1, Nodes: []openrtb2.SupplyChainNode{}, Ver: "2"}},
+				Regs:   &openrtb2.Regs{GDPR: openrtb2.Int8Ptr(1), USPrivacy: "3"},
+				User:   &openrtb2.User{Consent: "1", EIDs: []openrtb2.EID{{Source: "42"}}},
+			},
+			expectedRequest: openrtb2.BidRequest{
+				ID:     "anyID",
+				Source: &openrtb2.Source{Ext: json.RawMessage(`{"schain":{"complete":1,"nodes":[],"ver":"2"}}`)},
+				Regs:   &openrtb2.Regs{Ext: json.RawMessage(`{"gdpr":1,"us_privacy":"3"}`)},
+				User:   &openrtb2.User{Ext: json.RawMessage(`{"consent":"1","eids":[{"source":"42"}]}`)},
+			},
+		},
+		{
+			description: "2.6 -> 2.5 + Other Ext Fields",
+			givenRequest: openrtb2.BidRequest{
+				ID:     "anyID",
+				Ext:    json.RawMessage(`{"other":"otherExt"}`),
+				Source: &openrtb2.Source{SChain: &openrtb2.SupplyChain{Complete: 1, Nodes: []openrtb2.SupplyChainNode{}, Ver: "2"}, Ext: json.RawMessage(`{"other":"otherSource"}`)},
+				Regs:   &openrtb2.Regs{GDPR: openrtb2.Int8Ptr(1), USPrivacy: "3", Ext: json.RawMessage(`{"other":"otherRegs"}`)},
+				User:   &openrtb2.User{Consent: "1", EIDs: []openrtb2.EID{{Source: "42"}}, Ext: json.RawMessage(`{"other":"otherUser"}`)},
+			},
+			expectedRequest: openrtb2.BidRequest{
+				ID:     "anyID",
+				Ext:    json.RawMessage(`{"other":"otherExt"}`),
+				Source: &openrtb2.Source{Ext: json.RawMessage(`{"other":"otherSource","schain":{"complete":1,"nodes":[],"ver":"2"}}`)},
+				Regs:   &openrtb2.Regs{Ext: json.RawMessage(`{"gdpr":1,"other":"otherRegs","us_privacy":"3"}`)},
+				User:   &openrtb2.User{Ext: json.RawMessage(`{"consent":"1","eids":[{"source":"42"}],"other":"otherUser"}`)},
+			},
+		},
+		{
+			description: "Malformed - SChain",
+			givenRequest: openrtb2.BidRequest{
+				ID:     "anyID",
+				Source: &openrtb2.Source{SChain: &openrtb2.SupplyChain{Complete: 1, Nodes: []openrtb2.SupplyChainNode{}, Ver: "2"}, Ext: json.RawMessage(`malformed`)},
+			},
+			expectedErr: "invalid character 'm' looking for beginning of value",
+		},
+		{
+			description: "Malformed - GDPR",
+			givenRequest: openrtb2.BidRequest{
+				ID:   "anyID",
+				Regs: &openrtb2.Regs{GDPR: openrtb2.Int8Ptr(1), Ext: json.RawMessage(`malformed`)},
+			},
+			expectedErr: "invalid character 'm' looking for beginning of value",
+		},
+		{
+			description: "Malformed - Consent",
+			givenRequest: openrtb2.BidRequest{
+				ID:   "anyID",
+				User: &openrtb2.User{Consent: "1", Ext: json.RawMessage(`malformed`)},
+			},
+			expectedErr: "invalid character 'm' looking for beginning of value",
+		},
+		{
+			description: "Malformed - USPrivacy",
+			givenRequest: openrtb2.BidRequest{
+				ID:   "anyID",
+				Regs: &openrtb2.Regs{USPrivacy: "3", Ext: json.RawMessage(`malformed`)},
+			},
+			expectedErr: "invalid character 'm' looking for beginning of value",
+		},
+		{
+			description: "Malformed - EID",
+			givenRequest: openrtb2.BidRequest{
+				ID:   "anyID",
+				User: &openrtb2.User{EIDs: []openrtb2.EID{{Source: "42"}}, Ext: json.RawMessage(`malformed`)},
+			},
+			expectedErr: "invalid character 'm' looking for beginning of value",
+		},
+	}
+
+	for _, test := range testCases {
+		w := &RequestWrapper{BidRequest: &test.givenRequest}
+		err := ConvertDownTo25(w)
+		if len(test.expectedErr) > 0 {
+			assert.EqualError(t, err, test.expectedErr, test.description)
+		} else {
+			assert.NoError(t, w.RebuildRequest(), test.description)
+			assert.Equal(t, test.expectedRequest, *w.BidRequest, test.description)
+		}
+	}
+}
+
+func TestMoveSupplyChainFrom26To25(t *testing.T) {
+	var (
+		schain1     = &openrtb2.SupplyChain{Complete: 1, Nodes: []openrtb2.SupplyChainNode{}, Ver: "1"}
+		schain1Json = json.RawMessage(`{"schain":{"complete":1,"nodes":[],"ver":"1"}}`)
+		schain2Json = json.RawMessage(`{"schain":{"complete":1,"nodes":[],"ver":"2"}}`)
+	)
+
+	testCases := []struct {
+		description     string
+		givenRequest    openrtb2.BidRequest
+		expectedRequest openrtb2.BidRequest
+		expectedErr     string
+	}{
+		{
+			description:     "Not Present - Source",
+			givenRequest:    openrtb2.BidRequest{},
+			expectedRequest: openrtb2.BidRequest{},
+		},
+		{
+			description:     "Not Present - Source Schain",
+			givenRequest:    openrtb2.BidRequest{Source: &openrtb2.Source{}},
+			expectedRequest: openrtb2.BidRequest{Source: &openrtb2.Source{}},
+		},
+		{
+			description:     "2.6 Migrated To 2.5",
+			givenRequest:    openrtb2.BidRequest{Source: &openrtb2.Source{SChain: schain1}},
+			expectedRequest: openrtb2.BidRequest{Source: &openrtb2.Source{Ext: schain1Json}},
+		},
+		{
+			description:     "2.5 Overwritten",
+			givenRequest:    openrtb2.BidRequest{Source: &openrtb2.Source{SChain: schain1, Ext: schain2Json}},
+			expectedRequest: openrtb2.BidRequest{Source: &openrtb2.Source{Ext: schain1Json}},
+		},
+		{
+			description:  "Malformed",
+			givenRequest: openrtb2.BidRequest{Source: &openrtb2.Source{SChain: schain1, Ext: json.RawMessage(`malformed`)}},
+			expectedErr:  "invalid character 'm' looking for beginning of value",
+		},
+	}
+
+	for _, test := range testCases {
+		w := &RequestWrapper{BidRequest: &test.givenRequest}
+		err := moveSupplyChainFrom26To25(w)
+
+		if len(test.expectedErr) > 0 {
+			assert.EqualError(t, err, test.expectedErr, test.description)
+		} else {
+			assert.NoError(t, w.RebuildRequest(), test.description)
+			assert.Equal(t, test.expectedRequest, *w.BidRequest, test.description)
+		}
+	}
+}
+
+func TestMoveGDPRFrom26To25(t *testing.T) {
+	testCases := []struct {
+		description     string
+		givenRequest    openrtb2.BidRequest
+		expectedRequest openrtb2.BidRequest
+		expectedErr     string
+	}{
+		{
+			description:     "Not Present - Regs",
+			givenRequest:    openrtb2.BidRequest{},
+			expectedRequest: openrtb2.BidRequest{},
+		},
+		{
+			description:     "Not Present - Regs GDPR",
+			givenRequest:    openrtb2.BidRequest{Regs: &openrtb2.Regs{}},
+			expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{}},
+		},
+		{
+			description:     "2.6 Migrated To 2.5",
+			givenRequest:    openrtb2.BidRequest{Regs: &openrtb2.Regs{GDPR: openrtb2.Int8Ptr(0)}},
+			expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"gdpr":0}`)}},
+		},
+		{
+			description:     "2.5 Overwritten",
+			givenRequest:    openrtb2.BidRequest{Regs: &openrtb2.Regs{GDPR: openrtb2.Int8Ptr(0), Ext: json.RawMessage(`{"gdpr":1}`)}},
+			expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"gdpr":0}`)}},
+		},
+		{
+			description:  "Malformed",
+			givenRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{GDPR: openrtb2.Int8Ptr(0), Ext: json.RawMessage(`malformed`)}},
+			expectedErr:  "invalid character 'm' looking for beginning of value",
+		},
+	}
+
+	for _, test := range testCases {
+		w := &RequestWrapper{BidRequest: &test.givenRequest}
+		err := moveGDPRFrom26To25(w)
+
+		if len(test.expectedErr) > 0 {
+			assert.EqualError(t, err, test.expectedErr, test.description)
+		} else {
+			assert.NoError(t, w.RebuildRequest(), test.description)
+			assert.Equal(t, test.expectedRequest, *w.BidRequest, test.description)
+		}
+	}
+}
+
+func TestMoveConsentFrom26To25(t *testing.T) {
+	testCases := []struct {
+		description     string
+		givenRequest    openrtb2.BidRequest
+		expectedRequest openrtb2.BidRequest
+		expectedErr     string
+	}{
+		{
+			description:     "Not Present - User",
+			givenRequest:    openrtb2.BidRequest{},
+			expectedRequest: openrtb2.BidRequest{},
+		},
+		{
+			description:     "Not Present - User Consent",
+			givenRequest:    openrtb2.BidRequest{User: &openrtb2.User{}},
+			expectedRequest: openrtb2.BidRequest{User: &openrtb2.User{}},
+		},
+		{
+			description:     "2.6 Migrated To 2.5",
+			givenRequest:    openrtb2.BidRequest{User: &openrtb2.User{Consent: "1"}},
+			expectedRequest: openrtb2.BidRequest{User: &openrtb2.User{Ext: json.RawMessage(`{"consent":"1"}`)}},
+		},
+		{
+			description:     "2.5 Overwritten",
+			givenRequest:    openrtb2.BidRequest{User: &openrtb2.User{Consent: "1", Ext: json.RawMessage(`{"consent":"2"}`)}},
+			expectedRequest: openrtb2.BidRequest{User: &openrtb2.User{Ext: json.RawMessage(`{"consent":"1"}`)}},
+		},
+		{
+			description:  "Malformed",
+			givenRequest: openrtb2.BidRequest{User: &openrtb2.User{Consent: "1", Ext: json.RawMessage(`malformed`)}},
+			expectedErr:  "invalid character 'm' looking for beginning of value",
+		},
+	}
+
+	for _, test := range testCases {
+		w := &RequestWrapper{BidRequest: &test.givenRequest}
+		err := moveConsentFrom26To25(w)
+
+		if len(test.expectedErr) > 0 {
+			assert.EqualError(t, err, test.expectedErr, test.description)
+		} else {
+			assert.NoError(t, w.RebuildRequest(), test.description)
+			assert.Equal(t, test.expectedRequest, *w.BidRequest, test.description)
+		}
+	}
+}
+
+func TestMoveUSPrivacyFrom26To25(t *testing.T) {
+	testCases := []struct {
+		description     string
+		givenRequest    openrtb2.BidRequest
+		expectedRequest openrtb2.BidRequest
+		expectedErr     string
+	}{
+		{
+			description:     "Not Present - Regs",
+			givenRequest:    openrtb2.BidRequest{},
+			expectedRequest: openrtb2.BidRequest{},
+		},
+		{
+			description:     "Not Present - Regs USPrivacy",
+			givenRequest:    openrtb2.BidRequest{Regs: &openrtb2.Regs{}},
+			expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{}},
+		},
+		{
+			description:     "2.6 Migrated To 2.5",
+			givenRequest:    openrtb2.BidRequest{Regs: &openrtb2.Regs{USPrivacy: "1"}},
+			expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"us_privacy":"1"}`)}},
+		},
+		{
+			description:     "2.5 Overwritten",
+			givenRequest:    openrtb2.BidRequest{Regs: &openrtb2.Regs{USPrivacy: "1", Ext: json.RawMessage(`{"us_privacy":"2"}`)}},
+			expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"us_privacy":"1"}`)}},
+		},
+		{
+			description:  "Malformed",
+			givenRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{USPrivacy: "1", Ext: json.RawMessage(`malformed`)}},
+			expectedErr:  "invalid character 'm' looking for beginning of value",
+		},
+	}
+
+	for _, test := range testCases {
+		w := &RequestWrapper{BidRequest: &test.givenRequest}
+		err := moveUSPrivacyFrom26To25(w)
+
+		if len(test.expectedErr) > 0 {
+			assert.EqualError(t, err, test.expectedErr, test.description)
+		} else {
+			assert.NoError(t, w.RebuildRequest(), test.description)
+			assert.Equal(t, test.expectedRequest, *w.BidRequest, test.description)
+		}
+	}
+}
+
+func TestMoveEIDFrom26To25(t *testing.T) {
+	var (
+		eid1     = []openrtb2.EID{{Source: "1"}}
+		eid1Json = json.RawMessage(`{"eids":[{"source":"1"}]}`)
+		eid2Json = json.RawMessage(`{"eids":[{"source":"2"}]}`)
+	)
+
+	testCases := []struct {
+		description     string
+		givenRequest    openrtb2.BidRequest
+		expectedRequest openrtb2.BidRequest
+		expectedErr     string
+	}{
+		{
+			description:     "Not Present - User",
+			givenRequest:    openrtb2.BidRequest{},
+			expectedRequest: openrtb2.BidRequest{},
+		},
+		{
+			description:     "Not Present - User EIDs",
+			givenRequest:    openrtb2.BidRequest{User: &openrtb2.User{}},
+			expectedRequest: openrtb2.BidRequest{User: &openrtb2.User{}},
+		},
+		{
+			description:     "2.6 Migrated To 2.5",
+			givenRequest:    openrtb2.BidRequest{User: &openrtb2.User{EIDs: eid1}},
+			expectedRequest: openrtb2.BidRequest{User: &openrtb2.User{Ext: eid1Json}},
+		},
+		{
+			description:     "2.6 Migrated To 2.5 - Empty",
+			givenRequest:    openrtb2.BidRequest{User: &openrtb2.User{EIDs: []openrtb2.EID{}}},
+			expectedRequest: openrtb2.BidRequest{User: &openrtb2.User{}},
+		},
+		{
+			description:     "2.5 Overwritten",
+			givenRequest:    openrtb2.BidRequest{User: &openrtb2.User{EIDs: eid1, Ext: eid2Json}},
+			expectedRequest: openrtb2.BidRequest{User: &openrtb2.User{Ext: eid1Json}},
+		},
+		{
+			description:  "Malformed",
+			givenRequest: openrtb2.BidRequest{User: &openrtb2.User{EIDs: eid1, Ext: json.RawMessage(`malformed`)}},
+			expectedErr:  "invalid character 'm' looking for beginning of value",
+		},
+	}
+
+	for _, test := range testCases {
+		w := &RequestWrapper{BidRequest: &test.givenRequest}
+		err := moveEIDFrom26To25(w)
+
+		if len(test.expectedErr) > 0 {
+			assert.EqualError(t, err, test.expectedErr, test.description)
+		} else {
+			assert.NoError(t, w.RebuildRequest(), test.description)
+			assert.Equal(t, test.expectedRequest, *w.BidRequest, test.description)
+		}
+	}
+}
diff --git a/openrtb_ext/convert_up_test.go b/openrtb_ext/convert_up_test.go
new file mode 100644
index 00000000..075d4f82
--- /dev/null
+++ b/openrtb_ext/convert_up_test.go
@@ -0,0 +1,381 @@
+package openrtb_ext
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/mxmCherry/openrtb/v16/openrtb2"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestConvertUpTo26(t *testing.T) {
+	testCases := []struct {
+		description     string
+		givenRequest    openrtb2.BidRequest
+		expectedRequest openrtb2.BidRequest
+		expectedErr     string
+	}{
+		{
+			description: "Malformed",
+			givenRequest: openrtb2.BidRequest{
+				Ext: json.RawMessage(`malformed`),
+			},
+			expectedErr: "req.ext is invalid: invalid character 'm' looking for beginning of value",
+		},
+		{
+			description: "2.4 -> 2.6",
+			givenRequest: openrtb2.BidRequest{
+				ID:   "anyID",
+				Ext:  json.RawMessage(`{"schain":{"complete":1,"nodes":[],"ver":"2"}}`),
+				Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"gdpr":1,"us_privacy":"3"}`)},
+				User: &openrtb2.User{Ext: json.RawMessage(`{"consent":"1","eids":[{"source":"42"}]}`)},
+			},
+			expectedRequest: openrtb2.BidRequest{
+				ID:     "anyID",
+				Source: &openrtb2.Source{SChain: &openrtb2.SupplyChain{Complete: 1, Nodes: []openrtb2.SupplyChainNode{}, Ver: "2"}},
+				Regs:   &openrtb2.Regs{GDPR: openrtb2.Int8Ptr(1), USPrivacy: "3"},
+				User:   &openrtb2.User{Consent: "1", EIDs: []openrtb2.EID{{Source: "42"}}},
+			},
+		},
+		{
+			description: "2.4 -> 2.6 + Other Ext Fields",
+			givenRequest: openrtb2.BidRequest{
+				ID:   "anyID",
+				Ext:  json.RawMessage(`{"schain":{"complete":1,"nodes":[],"ver":"2"},"other":"otherExt"}`),
+				Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"gdpr":1,"other":"otherRegs","us_privacy":"3"}`)},
+				User: &openrtb2.User{Ext: json.RawMessage(`{"consent":"1","eids":[{"source":"42"}],"other":"otherUser"}`)},
+			},
+			expectedRequest: openrtb2.BidRequest{
+				ID:     "anyID",
+				Ext:    json.RawMessage(`{"other":"otherExt"}`),
+				Source: &openrtb2.Source{SChain: &openrtb2.SupplyChain{Complete: 1, Nodes: []openrtb2.SupplyChainNode{}, Ver: "2"}},
+				Regs:   &openrtb2.Regs{GDPR: openrtb2.Int8Ptr(1), USPrivacy: "3", Ext: json.RawMessage(`{"other":"otherRegs"}`)},
+				User:   &openrtb2.User{Consent: "1", EIDs: []openrtb2.EID{{Source: "42"}}, Ext: json.RawMessage(`{"other":"otherUser"}`)},
+			},
+		},
+		{
+			description: "2.5 -> 2.6",
+			givenRequest: openrtb2.BidRequest{
+				ID:     "anyID",
+				Source: &openrtb2.Source{Ext: json.RawMessage(`{"schain":{"complete":1,"nodes":[],"ver":"2"}}`)},
+				Regs:   &openrtb2.Regs{Ext: json.RawMessage(`{"gdpr":1,"us_privacy":"3"}`)},
+				User:   &openrtb2.User{Ext: json.RawMessage(`{"consent":"1","eids":[{"source":"42"}]}`)},
+			},
+			expectedRequest: openrtb2.BidRequest{
+				ID:     "anyID",
+				Source: &openrtb2.Source{SChain: &openrtb2.SupplyChain{Complete: 1, Nodes: []openrtb2.SupplyChainNode{}, Ver: "2"}},
+				Regs:   &openrtb2.Regs{GDPR: openrtb2.Int8Ptr(1), USPrivacy: "3"},
+				User:   &openrtb2.User{Consent: "1", EIDs: []openrtb2.EID{{Source: "42"}}},
+			},
+		},
+		{
+			description: "2.5 -> 2.6 + Other Ext Fields",
+			givenRequest: openrtb2.BidRequest{
+				ID:     "anyID",
+				Ext:    json.RawMessage(`{"other":"otherExt"}`),
+				Source: &openrtb2.Source{Ext: json.RawMessage(`{"schain":{"complete":1,"nodes":[],"ver":"2"},"other":"otherSource"}`)},
+				Regs:   &openrtb2.Regs{Ext: json.RawMessage(`{"gdpr":1,"us_privacy":"3","other":"otherRegs"}`)},
+				User:   &openrtb2.User{Ext: json.RawMessage(`{"consent":"1","eids":[{"source":"42"}],"other":"otherUser"}`)},
+			},
+			expectedRequest: openrtb2.BidRequest{
+				ID:     "anyID",
+				Ext:    json.RawMessage(`{"other":"otherExt"}`),
+				Source: &openrtb2.Source{SChain: &openrtb2.SupplyChain{Complete: 1, Nodes: []openrtb2.SupplyChainNode{}, Ver: "2"}, Ext: json.RawMessage(`{"other":"otherSource"}`)},
+				Regs:   &openrtb2.Regs{GDPR: openrtb2.Int8Ptr(1), USPrivacy: "3", Ext: json.RawMessage(`{"other":"otherRegs"}`)},
+				User:   &openrtb2.User{Consent: "1", EIDs: []openrtb2.EID{{Source: "42"}}, Ext: json.RawMessage(`{"other":"otherUser"}`)},
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		w := &RequestWrapper{BidRequest: &test.givenRequest}
+		err := ConvertUpTo26(w)
+		if len(test.expectedErr) > 0 {
+			assert.EqualError(t, err, test.expectedErr, test.description)
+		} else {
+			assert.NoError(t, w.RebuildRequest(), test.description)
+			assert.Equal(t, test.expectedRequest, *w.BidRequest, test.description)
+		}
+	}
+}
+
+func TestConvertUpEnsureExt(t *testing.T) {
+	testCases := []struct {
+		description  string
+		givenRequest openrtb2.BidRequest
+		expectedErr  string
+	}{
+		{
+			description:  "Empty",
+			givenRequest: openrtb2.BidRequest{},
+		},
+		{
+			description:  "Ext",
+			givenRequest: openrtb2.BidRequest{Ext: json.RawMessage("malformed")},
+			expectedErr:  "req.ext is invalid: invalid character 'm' looking for beginning of value",
+		},
+		{
+			description:  "Source.Ext",
+			givenRequest: openrtb2.BidRequest{Source: &openrtb2.Source{Ext: json.RawMessage("malformed")}},
+			expectedErr:  "req.source.ext is invalid: invalid character 'm' looking for beginning of value",
+		},
+		{
+			description:  "Regs.Ext",
+			givenRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage("malformed")}},
+			expectedErr:  "req.regs.ext is invalid: invalid character 'm' looking for beginning of value",
+		},
+		{
+			description:  "User.Ext",
+			givenRequest: openrtb2.BidRequest{User: &openrtb2.User{Ext: json.RawMessage("malformed")}},
+			expectedErr:  "req.user.ext is invalid: invalid character 'm' looking for beginning of value",
+		},
+	}
+
+	for _, test := range testCases {
+		w := &RequestWrapper{BidRequest: &test.givenRequest}
+		err := convertUpEnsureExt(w)
+		if len(test.expectedErr) > 0 {
+			assert.EqualError(t, err, test.expectedErr, test.description)
+		} else {
+			assert.NoError(t, err, test.description)
+		}
+	}
+}
+
+func TestMoveSupplyChainFrom24To25(t *testing.T) {
+	var (
+		schain1 = json.RawMessage(`{"schain":{"complete":1,"nodes":[],"ver":"1"}}`)
+		schain2 = json.RawMessage(`{"schain":{"complete":1,"nodes":[],"ver":"2"}}`)
+	)
+
+	testCases := []struct {
+		description     string
+		givenRequest    openrtb2.BidRequest
+		expectedRequest openrtb2.BidRequest
+	}{
+		{
+			description:     "Not Present",
+			givenRequest:    openrtb2.BidRequest{},
+			expectedRequest: openrtb2.BidRequest{},
+		},
+		{
+			description:     "2.4 Migrated To 2.5 - Source Doesn't Exist",
+			givenRequest:    openrtb2.BidRequest{Ext: schain1},
+			expectedRequest: openrtb2.BidRequest{Source: &openrtb2.Source{Ext: schain1}},
+		},
+		{
+			description:     "2.4 Migrated To 2.5 - Source Exists",
+			givenRequest:    openrtb2.BidRequest{Source: &openrtb2.Source{}, Ext: schain1},
+			expectedRequest: openrtb2.BidRequest{Source: &openrtb2.Source{Ext: schain1}},
+		},
+		{
+			description:     "2.4 Dropped",
+			givenRequest:    openrtb2.BidRequest{Source: &openrtb2.Source{Ext: schain1}, Ext: schain2},
+			expectedRequest: openrtb2.BidRequest{Source: &openrtb2.Source{Ext: schain1}},
+		},
+		{
+			description:     "2.5 Left Alone",
+			givenRequest:    openrtb2.BidRequest{Source: &openrtb2.Source{Ext: schain1}},
+			expectedRequest: openrtb2.BidRequest{Source: &openrtb2.Source{Ext: schain1}},
+		},
+	}
+
+	for _, test := range testCases {
+		w := &RequestWrapper{BidRequest: &test.givenRequest}
+		moveSupplyChainFrom24To25(w)
+		assert.NoError(t, w.RebuildRequest(), test.description)
+		assert.Equal(t, test.expectedRequest, *w.BidRequest, test.description)
+	}
+}
+
+func TestConvertSupplyChainFrom25To26(t *testing.T) {
+	var (
+		schain1     = &openrtb2.SupplyChain{Complete: 1, Nodes: []openrtb2.SupplyChainNode{}, Ver: "1"}
+		schain1Json = json.RawMessage(`{"schain":{"complete":1,"nodes":[],"ver":"1"}}`)
+		schain2Json = json.RawMessage(`{"schain":{"complete":1,"nodes":[],"ver":"2"}}`)
+	)
+
+	testCases := []struct {
+		description     string
+		givenRequest    openrtb2.BidRequest
+		expectedRequest openrtb2.BidRequest
+	}{
+		{
+			description:     "Not Present",
+			givenRequest:    openrtb2.BidRequest{},
+			expectedRequest: openrtb2.BidRequest{},
+		},
+		{
+			description:     "2.5 Migrated To 2.6",
+			givenRequest:    openrtb2.BidRequest{Source: &openrtb2.Source{Ext: schain1Json}},
+			expectedRequest: openrtb2.BidRequest{Source: &openrtb2.Source{SChain: schain1}},
+		},
+		{
+			description:     "2.5 Dropped",
+			givenRequest:    openrtb2.BidRequest{Source: &openrtb2.Source{SChain: schain1, Ext: schain2Json}},
+			expectedRequest: openrtb2.BidRequest{Source: &openrtb2.Source{SChain: schain1}},
+		},
+		{
+			description:     "2.6 Left Alone",
+			givenRequest:    openrtb2.BidRequest{Source: &openrtb2.Source{SChain: schain1}},
+			expectedRequest: openrtb2.BidRequest{Source: &openrtb2.Source{SChain: schain1}},
+		},
+	}
+
+	for _, test := range testCases {
+		w := &RequestWrapper{BidRequest: &test.givenRequest}
+		moveSupplyChainFrom25To26(w)
+		assert.NoError(t, w.RebuildRequest(), test.description)
+		assert.Equal(t, test.expectedRequest, *w.BidRequest, test.description)
+	}
+}
+
+func TestMoveGDPRFrom25To26(t *testing.T) {
+	testCases := []struct {
+		description     string
+		givenRequest    openrtb2.BidRequest
+		expectedRequest openrtb2.BidRequest
+	}{
+		{
+			description:     "Not Present",
+			givenRequest:    openrtb2.BidRequest{},
+			expectedRequest: openrtb2.BidRequest{},
+		},
+		{
+			description:     "2.5 Migrated To 2.6",
+			givenRequest:    openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"gdpr":0}`)}},
+			expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{GDPR: openrtb2.Int8Ptr(0)}},
+		},
+		{
+			description:     "2.5 Dropped",
+			givenRequest:    openrtb2.BidRequest{Regs: &openrtb2.Regs{GDPR: openrtb2.Int8Ptr(0), Ext: json.RawMessage(`{"gdpr":1}`)}},
+			expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{GDPR: openrtb2.Int8Ptr(0)}},
+		},
+		{
+			description:     "2.6 Left Alone",
+			givenRequest:    openrtb2.BidRequest{Regs: &openrtb2.Regs{GDPR: openrtb2.Int8Ptr(0)}},
+			expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{GDPR: openrtb2.Int8Ptr(0)}},
+		},
+	}
+
+	for _, test := range testCases {
+		w := &RequestWrapper{BidRequest: &test.givenRequest}
+		moveGDPRFrom25To26(w)
+		assert.NoError(t, w.RebuildRequest(), test.description)
+		assert.Equal(t, test.expectedRequest, *w.BidRequest, test.description)
+	}
+}
+
+func TestMoveConsentFrom25To26(t *testing.T) {
+	testCases := []struct {
+		description     string
+		givenRequest    openrtb2.BidRequest
+		expectedRequest openrtb2.BidRequest
+	}{
+		{
+			description:     "Not Present",
+			givenRequest:    openrtb2.BidRequest{},
+			expectedRequest: openrtb2.BidRequest{},
+		},
+		{
+			description:     "2.5 Migrated To 2.6",
+			givenRequest:    openrtb2.BidRequest{User: &openrtb2.User{Ext: json.RawMessage(`{"consent":"1"}`)}},
+			expectedRequest: openrtb2.BidRequest{User: &openrtb2.User{Consent: "1"}},
+		},
+		{
+			description:     "2.5 Dropped",
+			givenRequest:    openrtb2.BidRequest{User: &openrtb2.User{Consent: "1", Ext: json.RawMessage(`{"consent":2}`)}},
+			expectedRequest: openrtb2.BidRequest{User: &openrtb2.User{Consent: "1"}},
+		},
+		{
+			description:     "2.6 Left Alone",
+			givenRequest:    openrtb2.BidRequest{User: &openrtb2.User{Consent: "1"}},
+			expectedRequest: openrtb2.BidRequest{User: &openrtb2.User{Consent: "1"}},
+		},
+	}
+
+	for _, test := range testCases {
+		w := &RequestWrapper{BidRequest: &test.givenRequest}
+		moveConsentFrom25To26(w)
+		assert.NoError(t, w.RebuildRequest(), test.description)
+		assert.Equal(t, test.expectedRequest, *w.BidRequest, test.description)
+	}
+}
+
+func TestMoveUSPrivacyFrom25To26(t *testing.T) {
+	testCases := []struct {
+		description     string
+		givenRequest    openrtb2.BidRequest
+		expectedRequest openrtb2.BidRequest
+	}{
+		{
+			description:     "Not Present",
+			givenRequest:    openrtb2.BidRequest{},
+			expectedRequest: openrtb2.BidRequest{},
+		},
+		{
+			description:     "2.5 Migrated To 2.6",
+			givenRequest:    openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"us_privacy":"1"}`)}},
+			expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{USPrivacy: "1"}},
+		},
+		{
+			description:     "2.5 Dropped",
+			givenRequest:    openrtb2.BidRequest{Regs: &openrtb2.Regs{USPrivacy: "1", Ext: json.RawMessage(`{"us_privacy":"2"}`)}},
+			expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{USPrivacy: "1"}},
+		},
+		{
+			description:     "2.6 Left Alone",
+			givenRequest:    openrtb2.BidRequest{Regs: &openrtb2.Regs{USPrivacy: "1"}},
+			expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{USPrivacy: "1"}},
+		},
+	}
+
+	for _, test := range testCases {
+		w := &RequestWrapper{BidRequest: &test.givenRequest}
+		moveUSPrivacyFrom25To26(w)
+		assert.NoError(t, w.RebuildRequest(), test.description)
+		assert.Equal(t, test.expectedRequest, *w.BidRequest, test.description)
+	}
+}
+
+func TestMoveEIDFrom25To26(t *testing.T) {
+	var (
+		eid1     = []openrtb2.EID{{Source: "1"}}
+		eid1Json = json.RawMessage(`{"eids":[{"source":"1"}]}`)
+		eid2Json = json.RawMessage(`{"eids":[{"source":"2"}]}`)
+	)
+
+	testCases := []struct {
+		description     string
+		givenRequest    openrtb2.BidRequest
+		expectedRequest openrtb2.BidRequest
+	}{
+		{
+			description:     "Not Present",
+			givenRequest:    openrtb2.BidRequest{},
+			expectedRequest: openrtb2.BidRequest{},
+		},
+		{
+			description:     "2.5 Migrated To 2.6",
+			givenRequest:    openrtb2.BidRequest{User: &openrtb2.User{Ext: eid1Json}},
+			expectedRequest: openrtb2.BidRequest{User: &openrtb2.User{EIDs: eid1}},
+		},
+		{
+			description:     "2.5 Dropped",
+			givenRequest:    openrtb2.BidRequest{User: &openrtb2.User{EIDs: eid1, Ext: eid2Json}},
+			expectedRequest: openrtb2.BidRequest{User: &openrtb2.User{EIDs: eid1}},
+		},
+		{
+			description:     "2.6 Left Alone",
+			givenRequest:    openrtb2.BidRequest{User: &openrtb2.User{EIDs: eid1}},
+			expectedRequest: openrtb2.BidRequest{User: &openrtb2.User{EIDs: eid1}},
+		},
+	}
+
+	for _, test := range testCases {
+		w := &RequestWrapper{BidRequest: &test.givenRequest}
+		moveEIDFrom25To26(w)
+		assert.NoError(t, w.RebuildRequest(), test.description)
+		assert.Equal(t, test.expectedRequest, *w.BidRequest, test.description)
+	}
+}