Repository: prebid/prebid-server
Pull request #3708 (2024-05-28 22:08:07)

FPD: Don't Merge Arrays
The reflection based merge code merges elements within a json array, when instead the entire array should be overwritten.


Patch:
diff --git a/adapters/yieldmo/yieldmo.go b/adapters/yieldmo/yieldmo.go
index 6693861c..107622e9 100644
--- a/adapters/yieldmo/yieldmo.go
+++ b/adapters/yieldmo/yieldmo.go
@@ -4,6 +4,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"net/http"
+	"strings"
 
 	"github.com/prebid/openrtb/v20/openrtb2"
 	"github.com/prebid/prebid-server/v2/adapters"
@@ -35,16 +36,11 @@ type ExtBid struct {
 }
 
 func (a *YieldmoAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
-	reqData, errors := a.makeRequest(request)
-	return []*adapters.RequestData{reqData}, errors
-}
-
-func (a *YieldmoAdapter) makeRequest(request *openrtb2.BidRequest) (*adapters.RequestData, []error) {
 	var errs []error
 
-	if err := preprocess(request); err != nil {
-		errs = append(errs, err)
-	}
+	preprocessErrors := preprocess(request, reqInfo)
+
+	errs = append(errs, preprocessErrors...)
 
 	// Last Step
 	reqJSON, err := json.Marshal(request)
@@ -56,33 +52,47 @@ func (a *YieldmoAdapter) makeRequest(request *openrtb2.BidRequest) (*adapters.Re
 	headers := http.Header{}
 	headers.Add("Content-Type", "application/json;charset=utf-8")
 
-	return &adapters.RequestData{
+	return []*adapters.RequestData{{
 		Method:  "POST",
 		Uri:     a.endpoint,
 		Body:    reqJSON,
 		Headers: headers,
 		ImpIDs:  openrtb_ext.GetImpIDs(request.Imp),
-	}, errs
+	}}, errs
 }
 
 // Mutate the request to get it ready to send to yieldmo.
-func preprocess(request *openrtb2.BidRequest) error {
+func preprocess(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) []error {
+	var errs []error
+
 	for i := 0; i < len(request.Imp); i++ {
 		var imp = request.Imp[i]
 		var bidderExt ExtImpBidderYieldmo
 
+		if imp.BidFloor > 0 && imp.BidFloorCur != "" && strings.ToUpper(imp.BidFloorCur) != "USD" {
+			floor, err := reqInfo.ConvertCurrency(imp.BidFloor, imp.BidFloorCur, "USD")
+			if err != nil {
+				errs = append(errs, &errortypes.BadInput{
+					Message: fmt.Sprintf("Unable to convert provided bid floor currency from %s to USD", imp.BidFloorCur),
+				})
+			} else {
+				request.Imp[i].BidFloorCur = "USD"
+				request.Imp[i].BidFloor = floor
+			}
+		}
+
 		if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil {
-			return &errortypes.BadInput{
+			errs = append(errs, &errortypes.BadInput{
 				Message: err.Error(),
-			}
+			})
 		}
 
 		var yieldmoExt openrtb_ext.ExtImpYieldmo
 
 		if err := json.Unmarshal(bidderExt.Bidder, &yieldmoExt); err != nil {
-			return &errortypes.BadInput{
+			errs = append(errs, &errortypes.BadInput{
 				Message: err.Error(),
-			}
+			})
 		}
 
 		var impExt Ext
@@ -96,15 +106,15 @@ func preprocess(request *openrtb2.BidRequest) error {
 
 		impExtJSON, err := json.Marshal(impExt)
 		if err != nil {
-			return &errortypes.BadInput{
+			errs = append(errs, &errortypes.BadInput{
 				Message: err.Error(),
-			}
+			})
 		}
 
 		request.Imp[i].Ext = impExtJSON
 	}
 
-	return nil
+	return errs
 }
 
 // MakeBids make the bids for the bid response.
diff --git a/config/bidderinfo.go b/config/bidderinfo.go
index d78f5722..2151c159 100644
--- a/config/bidderinfo.go
+++ b/config/bidderinfo.go
@@ -245,7 +245,7 @@ func LoadBidderInfo(reader InfoReader) (BidderInfos, error) {
 	return processBidderInfos(reader, openrtb_ext.NormalizeBidderName)
 }
 
-func processBidderInfos(reader InfoReader, normalizeBidderName func(string) (openrtb_ext.BidderName, bool)) (BidderInfos, error) {
+func processBidderInfos(reader InfoReader, normalizeBidderName openrtb_ext.BidderNameNormalizer) (BidderInfos, error) {
 	bidderConfigs, err := reader.Read()
 	if err != nil {
 		return nil, fmt.Errorf("error loading bidders data")
@@ -583,7 +583,7 @@ func validateSyncer(bidderInfo BidderInfo) error {
 	return nil
 }
 
-func applyBidderInfoConfigOverrides(configBidderInfos nillableFieldBidderInfos, fsBidderInfos BidderInfos, normalizeBidderName func(string) (openrtb_ext.BidderName, bool)) (BidderInfos, error) {
+func applyBidderInfoConfigOverrides(configBidderInfos nillableFieldBidderInfos, fsBidderInfos BidderInfos, normalizeBidderName openrtb_ext.BidderNameNormalizer) (BidderInfos, error) {
 	mergedBidderInfos := make(map[string]BidderInfo, len(fsBidderInfos))
 
 	for bidderName, configBidderInfo := range configBidderInfos {
diff --git a/config/config.go b/config/config.go
index 4da788d4..bc1e17fb 100644
--- a/config/config.go
+++ b/config/config.go
@@ -714,7 +714,7 @@ func (cfg *TimeoutNotification) validate(errs []error) []error {
 }
 
 // New uses viper to get our server configurations.
-func New(v *viper.Viper, bidderInfos BidderInfos, normalizeBidderName func(string) (openrtb_ext.BidderName, bool)) (*Configuration, error) {
+func New(v *viper.Viper, bidderInfos BidderInfos, normalizeBidderName openrtb_ext.BidderNameNormalizer) (*Configuration, error) {
 	var c Configuration
 	if err := v.Unmarshal(&c); err != nil {
 		return nil, fmt.Errorf("viper failed to unmarshal app config: %v", err)
diff --git a/endpoints/events/vtrack.go b/endpoints/events/vtrack.go
index 01e544a4..bb479f2a 100644
--- a/endpoints/events/vtrack.go
+++ b/endpoints/events/vtrack.go
@@ -29,15 +29,13 @@ const (
 	ImpressionOpenTag    = "<Impression>"
 )
 
-type normalizeBidderName func(name string) (openrtb_ext.BidderName, bool)
-
 type vtrackEndpoint struct {
 	Cfg                 *config.Configuration
 	Accounts            stored_requests.AccountFetcher
 	BidderInfos         config.BidderInfos
 	Cache               prebid_cache_client.Client
 	MetricsEngine       metrics.MetricsEngine
-	normalizeBidderName normalizeBidderName
+	normalizeBidderName openrtb_ext.BidderNameNormalizer
 }
 
 type BidCacheRequest struct {
@@ -257,7 +255,7 @@ func (v *vtrackEndpoint) cachePutObjects(ctx context.Context, req *BidCacheReque
 }
 
 // getBiddersAllowingVastUpdate returns a list of bidders that allow VAST XML modification
-func getBiddersAllowingVastUpdate(req *BidCacheRequest, bidderInfos *config.BidderInfos, allowUnknownBidder bool, normalizeBidderName normalizeBidderName) map[string]struct{} {
+func getBiddersAllowingVastUpdate(req *BidCacheRequest, bidderInfos *config.BidderInfos, allowUnknownBidder bool, normalizeBidderName openrtb_ext.BidderNameNormalizer) map[string]struct{} {
 	bl := map[string]struct{}{}
 
 	for _, bcr := range req.Puts {
@@ -270,7 +268,7 @@ func getBiddersAllowingVastUpdate(req *BidCacheRequest, bidderInfos *config.Bidd
 }
 
 // isAllowVastForBidder checks if a bidder is active and allowed to modify vast xml data
-func isAllowVastForBidder(bidder string, bidderInfos *config.BidderInfos, allowUnknownBidder bool, normalizeBidderName normalizeBidderName) bool {
+func isAllowVastForBidder(bidder string, bidderInfos *config.BidderInfos, allowUnknownBidder bool, normalizeBidderName openrtb_ext.BidderNameNormalizer) bool {
 	// if bidder is active and isModifyingVastXmlAllowed is true
 	// check if bidder is configured
 	if normalizedBidder, ok := normalizeBidderName(bidder); ok {
diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go
index 9895f447..4a409a67 100644
--- a/endpoints/openrtb2/amp_auction.go
+++ b/endpoints/openrtb2/amp_auction.go
@@ -59,7 +59,7 @@ type ORTB2 struct {
 func NewAmpEndpoint(
 	uuidGenerator uuidutil.UUIDGenerator,
 	ex exchange.Exchange,
-	validator openrtb_ext.BidderParamValidator,
+	requestValidator ortb.RequestValidator,
 	requestsById stored_requests.Fetcher,
 	accounts stored_requests.AccountFetcher,
 	cfg *config.Configuration,
@@ -73,7 +73,7 @@ func NewAmpEndpoint(
 	tmaxAdjustments *exchange.TmaxAdjustmentsPreprocessed,
 ) (httprouter.Handle, error) {
 
-	if ex == nil || validator == nil || requestsById == nil || accounts == nil || cfg == nil || metricsEngine == nil {
+	if ex == nil || requestValidator == nil || requestsById == nil || accounts == nil || cfg == nil || metricsEngine == nil {
 		return nil, errors.New("NewAmpEndpoint requires non-nil arguments.")
 	}
 
@@ -87,7 +87,7 @@ func NewAmpEndpoint(
 	return httprouter.Handle((&endpointDeps{
 		uuidGenerator,
 		ex,
-		validator,
+		requestValidator,
 		requestsById,
 		empty_fetcher.EmptyFetcher{},
 		accounts,
@@ -236,8 +236,8 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h
 		errL = append(errL, errs...)
 	}
 
-	hasStoredResponses := len(storedAuctionResponses) > 0
-	errs := deps.validateRequest(account, r, reqWrapper, true, hasStoredResponses, storedBidResponses, false)
+	hasStoredAuctionResponses := len(storedAuctionResponses) > 0
+	errs := deps.validateRequest(account, r, reqWrapper, true, hasStoredAuctionResponses, storedBidResponses, false)
 	errL = append(errL, errs...)
 	ao.Errors = append(ao.Errors, errs...)
 	if errortypes.ContainsFatalError(errs) {
diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go
index a1d4a98f..d6ee5303 100644
--- a/endpoints/openrtb2/auction.go
+++ b/endpoints/openrtb2/auction.go
@@ -20,9 +20,6 @@ import (
 	"github.com/julienschmidt/httprouter"
 	gpplib "github.com/prebid/go-gpp"
 	"github.com/prebid/go-gpp/constants"
-	"github.com/prebid/openrtb/v20/adcom1"
-	"github.com/prebid/openrtb/v20/native1"
-	nativeRequests "github.com/prebid/openrtb/v20/native1/request"
 	"github.com/prebid/openrtb/v20/openrtb2"
 	"github.com/prebid/openrtb/v20/openrtb3"
 	"github.com/prebid/prebid-server/v2/bidadjustment"
@@ -88,7 +85,7 @@ var accountIdSearchPath = [...]struct {
 func NewEndpoint(
 	uuidGenerator uuidutil.UUIDGenerator,
 	ex exchange.Exchange,
-	validator openrtb_ext.BidderParamValidator,
+	requestValidator ortb.RequestValidator,
 	requestsById stored_requests.Fetcher,
 	accounts stored_requests.AccountFetcher,
 	cfg *config.Configuration,
@@ -101,7 +98,7 @@ func NewEndpoint(
 	hookExecutionPlanBuilder hooks.ExecutionPlanBuilder,
 	tmaxAdjustments *exchange.TmaxAdjustmentsPreprocessed,
 ) (httprouter.Handle, error) {
-	if ex == nil || validator == nil || requestsById == nil || accounts == nil || cfg == nil || metricsEngine == nil {
+	if ex == nil || requestValidator == nil || requestsById == nil || accounts == nil || cfg == nil || metricsEngine == nil {
 		return nil, errors.New("NewEndpoint requires non-nil arguments.")
 	}
 
@@ -115,7 +112,7 @@ func NewEndpoint(
 	return httprouter.Handle((&endpointDeps{
 		uuidGenerator,
 		ex,
-		validator,
+		requestValidator,
 		requestsById,
 		empty_fetcher.EmptyFetcher{},
 		accounts,
@@ -135,12 +132,10 @@ func NewEndpoint(
 		openrtb_ext.NormalizeBidderName}).Auction), nil
 }
 
-type normalizeBidderName func(name string) (openrtb_ext.BidderName, bool)
-
 type endpointDeps struct {
 	uuidGenerator             uuidutil.UUIDGenerator
 	ex                        exchange.Exchange
-	paramsValidator           openrtb_ext.BidderParamValidator
+	requestValidator          ortb.RequestValidator
 	storedReqFetcher          stored_requests.Fetcher
 	videoFetcher              stored_requests.Fetcher
 	accounts                  stored_requests.AccountFetcher
@@ -157,7 +152,7 @@ type endpointDeps struct {
 	storedRespFetcher         stored_requests.Fetcher
 	hookExecutionPlanBuilder  hooks.ExecutionPlanBuilder
 	tmaxAdjustments           *exchange.TmaxAdjustmentsPreprocessed
-	normalizeBidderName       normalizeBidderName
+	normalizeBidderName       openrtb_ext.BidderNameNormalizer
 }
 
 func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
@@ -567,8 +562,8 @@ func (deps *endpointDeps) parseRequest(httpRequest *http.Request, labels *metric
 		return nil, nil, nil, nil, nil, nil, errs
 	}
 
-	hasStoredResponses := len(storedAuctionResponses) > 0
-	errL = deps.validateRequest(account, httpRequest, req, false, hasStoredResponses, storedBidResponses, hasStoredBidRequest)
+	hasStoredAuctionResponses := len(storedAuctionResponses) > 0
+	errL = deps.validateRequest(account, httpRequest, req, false, hasStoredAuctionResponses, storedBidResponses, hasStoredBidRequest)
 	if len(errL) > 0 {
 		errs = append(errs, errL...)
 	}
@@ -673,7 +668,7 @@ func mergeBidderParamsImpExt(impExt *openrtb_ext.ImpExt, reqExtParams map[string
 	extMapModified := false
 
 	for bidder, params := range reqExtParams {
-		if !isPossibleBidder(bidder) {
+		if !openrtb_ext.IsPotentialBidder(bidder) {
 			continue
 		}
 
@@ -762,7 +757,7 @@ func mergeBidderParamsImpExtPrebid(impExt *openrtb_ext.ImpExt, reqExtParams map[
 	return nil
 }
 
-func (deps *endpointDeps) validateRequest(account *config.Account, httpReq *http.Request, req *openrtb_ext.RequestWrapper, isAmp bool, hasStoredResponses bool, storedBidResp stored_responses.ImpBidderStoredResp, hasStoredBidRequest bool) []error {
+func (deps *endpointDeps) validateRequest(account *config.Account, httpReq *http.Request, req *openrtb_ext.RequestWrapper, isAmp bool, hasStoredAuctionResponses bool, storedBidResp stored_responses.ImpBidderStoredResp, hasStoredBidRequest bool) []error {
 	errL := []error{}
 	if req.ID == "" {
 		return []error{errors.New("request missing required field: \"id\"")}
@@ -923,7 +918,7 @@ func (deps *endpointDeps) validateRequest(account *config.Account, httpReq *http
 		}
 		impIDs[imp.ID] = i
 
-		errs := deps.validateImp(imp, requestAliases, i, hasStoredResponses, storedBidResp)
+		errs := deps.requestValidator.ValidateImp(imp, i, requestAliases, hasStoredAuctionResponses, storedBidResp)
 		if len(errs) > 0 {
 			errL = append(errL, errs...)
 		}
@@ -1068,558 +1063,6 @@ func (deps *endpointDeps) validateBidders(bidders []string, knownBidders map[str
 	return nil
 }
 
-func (deps *endpointDeps) validateImp(imp *openrtb_ext.ImpWrapper, aliases map[string]string, index int, hasStoredResponses bool, storedBidResp stored_responses.ImpBidderStoredResp) []error {
-	if imp.ID == "" {
-		return []error{fmt.Errorf("request.imp[%d] missing required field: \"id\"", index)}
-	}
-
-	if len(imp.Metric) != 0 {
-		return []error{fmt.Errorf("request.imp[%d].metric is not yet supported by prebid-server. Support may be added in the future", index)}
-	}
-
-	if imp.Banner == nil && imp.Video == nil && imp.Audio == nil && imp.Native == nil {
-		return []error{fmt.Errorf("request.imp[%d] must contain at least one of \"banner\", \"video\", \"audio\", or \"native\"", index)}
-	}
-
-	if err := validateBanner(imp.Banner, index, isInterstitial(imp)); err != nil {
-		return []error{err}
-	}
-
-	if err := validateVideo(imp.Video, index); err != nil {
-		return []error{err}
-	}
-
-	if err := validateAudio(imp.Audio, index); err != nil {
-		return []error{err}
-	}
-
-	if err := fillAndValidateNative(imp.Native, index); err != nil {
-		return []error{err}
-	}
-
-	if err := validatePmp(imp.PMP, index); err != nil {
-		return []error{err}
-	}
-
-	errL := deps.validateImpExt(imp, aliases, index, hasStoredResponses, storedBidResp)
-	if len(errL) != 0 {
-		return errL
-	}
-
-	return nil
-}
-
-func isInterstitial(imp *openrtb_ext.ImpWrapper) bool {
-	return imp.Instl == 1
-}
-
-func validateBanner(banner *openrtb2.Banner, impIndex int, isInterstitial bool) error {
-	if banner == nil {
-		return nil
-	}
-
-	// The following fields were previously uints in the OpenRTB library we use, but have
-	// since been changed to ints. We decided to maintain the non-negative check.
-	if banner.W != nil && *banner.W < 0 {
-		return fmt.Errorf("request.imp[%d].banner.w must be a positive number", impIndex)
-	}
-	if banner.H != nil && *banner.H < 0 {
-		return fmt.Errorf("request.imp[%d].banner.h must be a positive number", impIndex)
-	}
-
-	// The following fields are deprecated in the OpenRTB 2.5 spec but are still present
-	// in the OpenRTB library we use. Enforce they are not specified.
-	if banner.WMin != 0 {
-		return fmt.Errorf("request.imp[%d].banner uses unsupported property: \"wmin\". Use the \"format\" array instead.", impIndex)
-	}
-	if banner.WMax != 0 {
-		return fmt.Errorf("request.imp[%d].banner uses unsupported property: \"wmax\". Use the \"format\" array instead.", impIndex)
-	}
-	if banner.HMin != 0 {
-		return fmt.Errorf("request.imp[%d].banner uses unsupported property: \"hmin\". Use the \"format\" array instead.", impIndex)
-	}
-	if banner.HMax != 0 {
-		return fmt.Errorf("request.imp[%d].banner uses unsupported property: \"hmax\". Use the \"format\" array instead.", impIndex)
-	}
-
-	hasRootSize := banner.H != nil && banner.W != nil && *banner.H > 0 && *banner.W > 0
-	if !hasRootSize && len(banner.Format) == 0 && !isInterstitial {
-		return fmt.Errorf("request.imp[%d].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements.", impIndex)
-	}
-
-	for i, format := range banner.Format {
-		if err := validateFormat(&format, impIndex, i); err != nil {
-			return err
-		}
-	}
-
-	return nil
-}
-
-func validateVideo(video *openrtb2.Video, impIndex int) error {
-	if video == nil {
-		return nil
-	}
-
-	if len(video.MIMEs) < 1 {
-		return fmt.Errorf("request.imp[%d].video.mimes must contain at least one supported MIME type", impIndex)
-	}
-
-	// The following fields were previously uints in the OpenRTB library we use, but have
-	// since been changed to ints. We decided to maintain the non-negative check.
-	if video.W != nil && *video.W < 0 {
-		return fmt.Errorf("request.imp[%d].video.w must be a positive number", impIndex)
-	}
-	if video.H != nil && *video.H < 0 {
-		return fmt.Errorf("request.imp[%d].video.h must be a positive number", impIndex)
-	}
-	if video.MinBitRate < 0 {
-		return fmt.Errorf("request.imp[%d].video.minbitrate must be a positive number", impIndex)
-	}
-	if video.MaxBitRate < 0 {
-		return fmt.Errorf("request.imp[%d].video.maxbitrate must be a positive number", impIndex)
-	}
-
-	return nil
-}
-
-func validateAudio(audio *openrtb2.Audio, impIndex int) error {
-	if audio == nil {
-		return nil
-	}
-
-	if len(audio.MIMEs) < 1 {
-		return fmt.Errorf("request.imp[%d].audio.mimes must contain at least one supported MIME type", impIndex)
-	}
-
-	// The following fields were previously uints in the OpenRTB library we use, but have
-	// since been changed to ints. We decided to maintain the non-negative check.
-	if audio.Sequence < 0 {
-		return fmt.Errorf("request.imp[%d].audio.sequence must be a positive number", impIndex)
-	}
-	if audio.MaxSeq < 0 {
-		return fmt.Errorf("request.imp[%d].audio.maxseq must be a positive number", impIndex)
-	}
-	if audio.MinBitrate < 0 {
-		return fmt.Errorf("request.imp[%d].audio.minbitrate must be a positive number", impIndex)
-	}
-	if audio.MaxBitrate < 0 {
-		return fmt.Errorf("request.imp[%d].audio.maxbitrate must be a positive number", impIndex)
-	}
-
-	return nil
-}
-
-// fillAndValidateNative validates the request, and assigns the Asset IDs as recommended by the Native v1.2 spec.
-func fillAndValidateNative(n *openrtb2.Native, impIndex int) error {
-	if n == nil {
-		return nil
-	}
-
-	if len(n.Request) == 0 {
-		return fmt.Errorf("request.imp[%d].native missing required property \"request\"", impIndex)
-	}
-	var nativePayload nativeRequests.Request
-	if err := jsonutil.UnmarshalValid(json.RawMessage(n.Request), &nativePayload); err != nil {
-		return err
-	}
-
-	if err := validateNativeContextTypes(nativePayload.Context, nativePayload.ContextSubType, impIndex); err != nil {
-		return err
-	}
-	if err := validateNativePlacementType(nativePayload.PlcmtType, impIndex); err != nil {
-		return err
-	}
-	if err := fillAndValidateNativeAssets(nativePayload.Assets, impIndex); err != nil {
-		return err
-	}
-	if err := validateNativeEventTrackers(nativePayload.EventTrackers, impIndex); err != nil {
-		return err
-	}
-
-	serialized, err := jsonutil.Marshal(nativePayload)
-	if err != nil {
-		return err
-	}
-	n.Request = string(serialized)
-	return nil
-}
-
-func validateNativeContextTypes(cType native1.ContextType, cSubtype native1.ContextSubType, impIndex int) error {
-	if cType == 0 {
-		// Context is only recommended, so none is a valid type.
-		return nil
-	}
-	if cType < native1.ContextTypeContent || (cType > native1.ContextTypeProduct && cType < openrtb_ext.NativeExchangeSpecificLowerBound) {
-		return fmt.Errorf("request.imp[%d].native.request.context is invalid. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39", impIndex)
-	}
-	if cSubtype < 0 {
-		return fmt.Errorf("request.imp[%d].native.request.contextsubtype value can't be less than 0. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39", impIndex)
-	}
-	if cSubtype == 0 {
-		return nil
-	}
-	if cSubtype >= native1.ContextSubTypeGeneral && cSubtype <= native1.ContextSubTypeUserGenerated {
-		if cType != native1.ContextTypeContent && cType < openrtb_ext.NativeExchangeSpecificLowerBound {
-			return fmt.Errorf("request.imp[%d].native.request.context is %d, but contextsubtype is %d. This is an invalid combination. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39", impIndex, cType, cSubtype)
-		}
-		return nil
-	}
-	if cSubtype >= native1.ContextSubTypeSocial && cSubtype <= native1.ContextSubTypeChat {
-		if cType != native1.ContextTypeSocial && cType < openrtb_ext.NativeExchangeSpecificLowerBound {
-			return fmt.Errorf("request.imp[%d].native.request.context is %d, but contextsubtype is %d. This is an invalid combination. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39", impIndex, cType, cSubtype)
-		}
-		return nil
-	}
-	if cSubtype >= native1.ContextSubTypeSelling && cSubtype <= native1.ContextSubTypeProductReview {
-		if cType != native1.ContextTypeProduct && cType < openrtb_ext.NativeExchangeSpecificLowerBound {
-			return fmt.Errorf("request.imp[%d].native.request.context is %d, but contextsubtype is %d. This is an invalid combination. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39", impIndex, cType, cSubtype)
-		}
-		return nil
-	}
-	if cSubtype >= openrtb_ext.NativeExchangeSpecificLowerBound {
-		return nil
-	}
-
-	return fmt.Errorf("request.imp[%d].native.request.contextsubtype is invalid. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39", impIndex)
-}
-
-func validateNativePlacementType(pt native1.PlacementType, impIndex int) error {
-	if pt == 0 {
-		// Placement Type is only recommended, not required.
-		return nil
-	}
-	if pt < native1.PlacementTypeFeed || (pt > native1.PlacementTypeRecommendationWidget && pt < openrtb_ext.NativeExchangeSpecificLowerBound) {
-		return fmt.Errorf("request.imp[%d].native.request.plcmttype is invalid. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=40", impIndex)
-	}
-	return nil
-}
-
-func fillAndValidateNativeAssets(assets []nativeRequests.Asset, impIndex int) error {
-	if len(assets) < 1 {
-		return fmt.Errorf("request.imp[%d].native.request.assets must be an array containing at least one object", impIndex)
-	}
-
-	assetIDs := make(map[int64]struct{}, len(assets))
-
-	// If none of the asset IDs are defined by the caller, then prebid server should assign its own unique IDs. But
-	// if the caller did assign its own asset IDs, then prebid server will respect those IDs
-	assignAssetIDs := true
-	for i := 0; i < len(assets); i++ {
-		assignAssetIDs = assignAssetIDs && (assets[i].ID == 0)
-	}
-
-	for i := 0; i < len(assets); i++ {
-		if err := validateNativeAsset(assets[i], impIndex, i); err != nil {
-			return err
-		}
-
-		if assignAssetIDs {
-			assets[i].ID = int64(i)
-			continue
-		}
-
-		// Each asset should have a unique ID thats assigned by the caller
-		if _, ok := assetIDs[assets[i].ID]; ok {
-			return fmt.Errorf("request.imp[%d].native.request.assets[%d].id is already being used by another asset. Each asset ID must be unique.", impIndex, i)
-		}
-
-		assetIDs[assets[i].ID] = struct{}{}
-	}
-
-	return nil
-}
-
-func validateNativeAsset(asset nativeRequests.Asset, impIndex int, assetIndex int) error {
-	assetErr := "request.imp[%d].native.request.assets[%d] must define exactly one of {title, img, video, data}"
-	foundType := false
-
-	if asset.Title != nil {
-		foundType = true
-		if err := validateNativeAssetTitle(asset.Title, impIndex, assetIndex); err != nil {
-			return err
-		}
-	}
-
-	if asset.Img != nil {
-		if foundType {
-			return fmt.Errorf(assetErr, impIndex, assetIndex)
-		}
-		foundType = true
-		if err := validateNativeAssetImage(asset.Img, impIndex, assetIndex); err != nil {
-			return err
-		}
-	}
-
-	if asset.Video != nil {
-		if foundType {
-			return fmt.Errorf(assetErr, impIndex, assetIndex)
-		}
-		foundType = true
-		if err := validateNativeAssetVideo(asset.Video, impIndex, assetIndex); err != nil {
-			return err
-		}
-	}
-
-	if asset.Data != nil {
-		if foundType {
-			return fmt.Errorf(assetErr, impIndex, assetIndex)
-		}
-		foundType = true
-		if err := validateNativeAssetData(asset.Data, impIndex, assetIndex); err != nil {
-			return err
-		}
-	}
-
-	if !foundType {
-		return fmt.Errorf(assetErr, impIndex, assetIndex)
-	}
-
-	return nil
-}
-
-func validateNativeEventTrackers(trackers []nativeRequests.EventTracker, impIndex int) error {
-	for i := 0; i < len(trackers); i++ {
-		if err := validateNativeEventTracker(trackers[i], impIndex, i); err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-func validateNativeAssetTitle(title *nativeRequests.Title, impIndex int, assetIndex int) error {
-	if title.Len < 1 {
-		return fmt.Errorf("request.imp[%d].native.request.assets[%d].title.len must be a positive number", impIndex, assetIndex)
-	}
-	return nil
-}
-
-func validateNativeEventTracker(tracker nativeRequests.EventTracker, impIndex int, eventIndex int) error {
-	if tracker.Event < native1.EventTypeImpression || (tracker.Event > native1.EventTypeViewableVideo50 && tracker.Event < openrtb_ext.NativeExchangeSpecificLowerBound) {
-		return fmt.Errorf("request.imp[%d].native.request.eventtrackers[%d].event is invalid. See section 7.6: https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=43", impIndex, eventIndex)
-	}
-	if len(tracker.Methods) < 1 {
-		return fmt.Errorf("request.imp[%d].native.request.eventtrackers[%d].method is required. See section 7.7: https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=43", impIndex, eventIndex)
-	}
-	for methodIndex, method := range tracker.Methods {
-		if method < native1.EventTrackingMethodImage || (method > native1.EventTrackingMethodJS && method < openrtb_ext.NativeExchangeSpecificLowerBound) {
-			return fmt.Errorf("request.imp[%d].native.request.eventtrackers[%d].methods[%d] is invalid. See section 7.7: https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=43", impIndex, eventIndex, methodIndex)
-		}
-	}
-
-	return nil
-}
-
-func validateNativeAssetImage(img *nativeRequests.Image, impIndex int, assetIndex int) error {
-	if img.W < 0 {
-		return fmt.Errorf("request.imp[%d].native.request.assets[%d].img.w must be a positive integer", impIndex, assetIndex)
-	}
-	if img.H < 0 {
-		return fmt.Errorf("request.imp[%d].native.request.assets[%d].img.h must be a positive integer", impIndex, assetIndex)
-	}
-	if img.WMin < 0 {
-		return fmt.Errorf("request.imp[%d].native.request.assets[%d].img.wmin must be a positive integer", impIndex, assetIndex)
-	}
-	if img.HMin < 0 {
-		return fmt.Errorf("request.imp[%d].native.request.assets[%d].img.hmin must be a positive integer", impIndex, assetIndex)
-	}
-	return nil
-}
-
-func validateNativeAssetVideo(video *nativeRequests.Video, impIndex int, assetIndex int) error {
-	if len(video.MIMEs) < 1 {
-		return fmt.Errorf("request.imp[%d].native.request.assets[%d].video.mimes must be an array with at least one MIME type", impIndex, assetIndex)
-	}
-	if video.MinDuration < 1 {
-		return fmt.Errorf("request.imp[%d].native.request.assets[%d].video.minduration must be a positive integer", impIndex, assetIndex)
-	}
-	if video.MaxDuration < 1 {
-		return fmt.Errorf("request.imp[%d].native.request.assets[%d].video.maxduration must be a positive integer", impIndex, assetIndex)
-	}
-	if err := validateNativeVideoProtocols(video.Protocols, impIndex, assetIndex); err != nil {
-		return err
-	}
-
-	return nil
-}
-
-func validateNativeAssetData(data *nativeRequests.Data, impIndex int, assetIndex int) error {
-	if data.Type < native1.DataAssetTypeSponsored || (data.Type > native1.DataAssetTypeCTAText && data.Type < 500) {
-		return fmt.Errorf("request.imp[%d].native.request.assets[%d].data.type is invalid. See section 7.4: https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=40", impIndex, assetIndex)
-	}
-
-	return nil
-}
-
-func validateNativeVideoProtocols(protocols []adcom1.MediaCreativeSubtype, impIndex int, assetIndex int) error {
-	if len(protocols) < 1 {
-		return fmt.Errorf("request.imp[%d].native.request.assets[%d].video.protocols must be an array with at least one element", impIndex, assetIndex)
-	}
-	for i := 0; i < len(protocols); i++ {
-		if err := validateNativeVideoProtocol(protocols[i], impIndex, assetIndex, i); err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-func validateNativeVideoProtocol(protocol adcom1.MediaCreativeSubtype, impIndex int, assetIndex int, protocolIndex int) error {
-	if protocol < adcom1.CreativeVAST10 || protocol > adcom1.CreativeDAAST10Wrapper {
-		return fmt.Errorf("request.imp[%d].native.request.assets[%d].video.protocols[%d] is invalid. See Section 5.8: https://www.iab.com/wp-content/uploads/2016/03/OpenRTB-API-Specification-Version-2-5-FINAL.pdf#page=52", impIndex, assetIndex, protocolIndex)
-	}
-	return nil
-}
-
-func validateFormat(format *openrtb2.Format, impIndex, formatIndex int) error {
-	usesHW := format.W != 0 || format.H != 0
-	usesRatios := format.WMin != 0 || format.WRatio != 0 || format.HRatio != 0
-
-	// The following fields were previously uints in the OpenRTB library we use, but have
-	// since been changed to ints. We decided to maintain the non-negative check.
-	if format.W < 0 {
-		return fmt.Errorf("request.imp[%d].banner.format[%d].w must be a positive number", impIndex, formatIndex)
-	}
-	if format.H < 0 {
-		return fmt.Errorf("request.imp[%d].banner.format[%d].h must be a positive number", impIndex, formatIndex)
-	}
-	if format.WRatio < 0 {
-		return fmt.Errorf("request.imp[%d].banner.format[%d].wratio must be a positive number", impIndex, formatIndex)
-	}
-	if format.HRatio < 0 {
-		return fmt.Errorf("request.imp[%d].banner.format[%d].hratio must be a positive number", impIndex, formatIndex)
-	}
-	if format.WMin < 0 {
-		return fmt.Errorf("request.imp[%d].banner.format[%d].wmin must be a positive number", impIndex, formatIndex)
-	}
-
-	if usesHW && usesRatios {
-		return fmt.Errorf("Request imp[%d].banner.format[%d] should define *either* {w, h} *or* {wmin, wratio, hratio}, but not both. If both are valid, send two \"format\" objects in the request.", impIndex, formatIndex)
-	}
-	if !usesHW && !usesRatios {
-		return fmt.Errorf("Request imp[%d].banner.format[%d] should define *either* {w, h} (for static size requirements) *or* {wmin, wratio, hratio} (for flexible sizes) to be non-zero.", impIndex, formatIndex)
-	}
-	if usesHW && (format.W == 0 || format.H == 0) {
-		return fmt.Errorf("Request imp[%d].banner.format[%d] must define non-zero \"h\" and \"w\" properties.", impIndex, formatIndex)
-	}
-	if usesRatios && (format.WMin == 0 || format.WRatio == 0 || format.HRatio == 0) {
-		return fmt.Errorf("Request imp[%d].banner.format[%d] must define non-zero \"wmin\", \"wratio\", and \"hratio\" properties.", impIndex, formatIndex)
-	}
-	return nil
-}
-
-func validatePmp(pmp *openrtb2.PMP, impIndex int) error {
-	if pmp == nil {
-		return nil
-	}
-
-	for dealIndex, deal := range pmp.Deals {
-		if deal.ID == "" {
-			return fmt.Errorf("request.imp[%d].pmp.deals[%d] missing required field: \"id\"", impIndex, dealIndex)
-		}
-	}
-	return nil
-}
-
-func (deps *endpointDeps) validateImpExt(imp *openrtb_ext.ImpWrapper, aliases map[string]string, impIndex int, hasStoredResponses bool, storedBidResp stored_responses.ImpBidderStoredResp) []error {
-	if len(imp.Ext) == 0 {
-		return []error{fmt.Errorf("request.imp[%d].ext is required", impIndex)}
-	}
-
-	impExt, err := imp.GetImpExt()
-	if err != nil {
-		return []error{err}
-	}
-
-	prebid := impExt.GetOrCreatePrebid()
-	prebidModified := false
-
-	if prebid.Bidder == nil {
-		prebid.Bidder = make(map[string]json.RawMessage)
-	}
-
-	ext := impExt.GetExt()
-	extModified := false
-
-	// promote imp[].ext.BIDDER to newer imp[].ext.prebid.bidder.BIDDER location, with the later taking precedence
-	for k, v := range ext {
-		if isPossibleBidder(k) {
-			if _, exists := prebid.Bidder[k]; !exists {
-				prebid.Bidder[k] = v
-				prebidModified = true
-			}
-			delete(ext, k)
-			extModified = true
-		}
-	}
-
-	if hasStoredResponses && prebid.StoredAuctionResponse == nil {
-		return []error{fmt.Errorf("request validation failed. The StoredAuctionResponse.ID field must be completely present with, or completely absent from, all impressions in request. No StoredAuctionResponse data found for request.imp[%d].ext.prebid \n", impIndex)}
-	}
-
-	if err := deps.validateStoredBidRespAndImpExtBidders(prebid, storedBidResp, imp.ID); err != nil {
-		return []error{err}
-	}
-
-	errL := []error{}
-
-	for bidder, ext := range prebid.Bidder {
-		coreBidder, _ := openrtb_ext.NormalizeBidderName(bidder)
-		if tmp, isAlias := aliases[bidder]; isAlias {
-			coreBidder = openrtb_ext.BidderName(tmp)
-		}
-
-		if coreBidderNormalized, isValid := deps.bidderMap[coreBidder.String()]; isValid {
-			if err := deps.paramsValidator.Validate(coreBidderNormalized, ext); err != nil {
-				return []error{fmt.Errorf("request.imp[%d].ext.prebid.bidder.%s failed validation.\n%v", impIndex, bidder, err)}
-			}
-		} else {
-			if msg, isDisabled := deps.disabledBidders[bidder]; isDisabled {
-				errL = append(errL, &errortypes.BidderTemporarilyDisabled{Message: msg})
-				delete(prebid.Bidder, bidder)
-				prebidModified = true
-			} else {
-				return []error{fmt.Errorf("request.imp[%d].ext.prebid.bidder contains unknown bidder: %s. Did you forget an alias in request.ext.prebid.aliases?", impIndex, bidder)}
-			}
-		}
-	}
-
-	if len(prebid.Bidder) == 0 {
-		errL = append(errL, fmt.Errorf("request.imp[%d].ext.prebid.bidder must contain at least one bidder", impIndex))
-		return errL
-	}
-
-	if prebidModified {
-		impExt.SetPrebid(prebid)
-	}
-	if extModified {
-		impExt.SetExt(ext)
-	}
-
-	return errL
-}
-
-// isPossibleBidder determines if a bidder name is a potential real bidder.
-func isPossibleBidder(bidder string) bool {
-	switch openrtb_ext.BidderName(bidder) {
-	case openrtb_ext.BidderReservedContext:
-		return false
-	case openrtb_ext.BidderReservedData:
-		return false
-	case openrtb_ext.BidderReservedGPID:
-		return false
-	case openrtb_ext.BidderReservedPrebid:
-		return false
-	case openrtb_ext.BidderReservedSKAdN:
-		return false
-	case openrtb_ext.BidderReservedTID:
-		return false
-	case openrtb_ext.BidderReservedAE:
-		return false
-	default:
-		return true
-	}
-}
-
 func (deps *endpointDeps) parseBidExt(req *openrtb_ext.RequestWrapper) error {
 	if _, err := req.GetRequestExt(); err != nil {
 		return fmt.Errorf("request.ext is invalid: %v", err)
@@ -2568,32 +2011,3 @@ func checkIfAppRequest(request []byte) (bool, error) {
 	}
 	return false, nil
 }
-
-func (deps *endpointDeps) validateStoredBidRespAndImpExtBidders(prebid *openrtb_ext.ExtImpPrebid, storedBidResp stored_responses.ImpBidderStoredResp, impId string) error {
-	if storedBidResp == nil && len(prebid.StoredBidResponse) == 0 {
-		return nil
-	}
-
-	if storedBidResp == nil {
-		return generateStoredBidResponseValidationError(impId)
-	}
-	if bidResponses, ok := storedBidResp[impId]; ok {
-		if len(bidResponses) != len(prebid.Bidder) {
-			return generateStoredBidResponseValidationError(impId)
-		}
-
-		for bidderName := range bidResponses {
-			if _, bidderNameOk := deps.normalizeBidderName(bidderName); !bidderNameOk {
-				return fmt.Errorf(`unrecognized bidder "%v"`, bidderName)
-			}
-			if _, present := prebid.Bidder[bidderName]; !present {
-				return generateStoredBidResponseValidationError(impId)
-			}
-		}
-	}
-	return nil
-}
-
-func generateStoredBidResponseValidationError(impID string) error {
-	return fmt.Errorf("request validation failed. Stored bid responses are specified for imp %s. Bidders specified in imp.ext should match with bidders specified in imp.ext.prebid.storedbidresponse", impID)
-}
diff --git a/endpoints/openrtb2/video_auction.go b/endpoints/openrtb2/video_auction.go
index 451e1f90..2a9885e2 100644
--- a/endpoints/openrtb2/video_auction.go
+++ b/endpoints/openrtb2/video_auction.go
@@ -46,7 +46,7 @@ var defaultRequestTimeout int64 = 5000
 func NewVideoEndpoint(
 	uuidGenerator uuidutil.UUIDGenerator,
 	ex exchange.Exchange,
-	validator openrtb_ext.BidderParamValidator,
+	requestValidator ortb.RequestValidator,
 	requestsById stored_requests.Fetcher,
 	videoFetcher stored_requests.Fetcher,
 	accounts stored_requests.AccountFetcher,
@@ -60,7 +60,7 @@ func NewVideoEndpoint(
 	tmaxAdjustments *exchange.TmaxAdjustmentsPreprocessed,
 ) (httprouter.Handle, error) {
 
-	if ex == nil || validator == nil || requestsById == nil || accounts == nil || cfg == nil || met == nil {
+	if ex == nil || requestValidator == nil || requestsById == nil || accounts == nil || cfg == nil || met == nil {
 		return nil, errors.New("NewVideoEndpoint requires non-nil arguments.")
 	}
 
@@ -76,7 +76,7 @@ func NewVideoEndpoint(
 	return httprouter.Handle((&endpointDeps{
 		uuidGenerator,
 		ex,
-		validator,
+		requestValidator,
 		requestsById,
 		videoFetcher,
 		accounts,
diff --git a/errortypes/code.go b/errortypes/code.go
index a56df67c..552beffb 100644
--- a/errortypes/code.go
+++ b/errortypes/code.go
@@ -17,6 +17,7 @@ const (
 	TmaxTimeoutErrorCode
 	FailedToMarshalErrorCode
 	FailedToUnmarshalErrorCode
+	InvalidImpFirstPartyDataErrorCode
 )
 
 // Defines numeric codes for well-known warnings.
diff --git a/errortypes/errortypes.go b/errortypes/errortypes.go
index 7ca5668c..f23e1423 100644
--- a/errortypes/errortypes.go
+++ b/errortypes/errortypes.go
@@ -274,3 +274,21 @@ func (err *DebugWarning) Severity() Severity {
 func (err *DebugWarning) Scope() Scope {
 	return ScopeDebug
 }
+
+// InvalidImpFirstPartyData should be used when the retrieved account config cannot be unmarshaled
+// These errors will be written to http.ResponseWriter before canceling execution
+type InvalidImpFirstPartyData struct {
+	Message string
+}
+
+func (err *InvalidImpFirstPartyData) Error() string {
+	return err.Message
+}
+
+func (err *InvalidImpFirstPartyData) Code() int {
+	return InvalidImpFirstPartyDataErrorCode
+}
+
+func (err *InvalidImpFirstPartyData) Severity() Severity {
+	return SeverityFatal
+}
diff --git a/exchange/exchange.go b/exchange/exchange.go
index 01abe37d..5c27b0d3 100644
--- a/exchange/exchange.go
+++ b/exchange/exchange.go
@@ -14,6 +14,7 @@ import (
 	"strings"
 	"time"
 
+	"github.com/prebid/prebid-server/v2/ortb"
 	"github.com/prebid/prebid-server/v2/privacy"
 
 	"github.com/prebid/prebid-server/v2/adapters"
@@ -132,7 +133,7 @@ func (randomDeduplicateBidBooleanGenerator) Generate() bool {
 	return rand.Intn(100) < 50
 }
 
-func NewExchange(adapters map[openrtb_ext.BidderName]AdaptedBidder, cache prebid_cache_client.Client, cfg *config.Configuration, syncersByBidder map[string]usersync.Syncer, metricsEngine metrics.MetricsEngine, infos config.BidderInfos, gdprPermsBuilder gdpr.PermissionsBuilder, currencyConverter *currency.RateConverter, categoriesFetcher stored_requests.CategoryFetcher, adsCertSigner adscert.Signer, macroReplacer macros.Replacer, priceFloorFetcher floors.FloorFetcher) Exchange {
+func NewExchange(adapters map[openrtb_ext.BidderName]AdaptedBidder, cache prebid_cache_client.Client, cfg *config.Configuration, requestValidator ortb.RequestValidator, syncersByBidder map[string]usersync.Syncer, metricsEngine metrics.MetricsEngine, infos config.BidderInfos, gdprPermsBuilder gdpr.PermissionsBuilder, currencyConverter *currency.RateConverter, categoriesFetcher stored_requests.CategoryFetcher, adsCertSigner adscert.Signer, macroReplacer macros.Replacer, priceFloorFetcher floors.FloorFetcher) Exchange {
 	bidderToSyncerKey := map[string]string{}
 	for bidder, syncer := range syncersByBidder {
 		bidderToSyncerKey[bidder] = syncer.Key()
@@ -155,6 +156,7 @@ func NewExchange(adapters map[openrtb_ext.BidderName]AdaptedBidder, cache prebid
 		gdprPermsBuilder:  gdprPermsBuilder,
 		hostSChainNode:    cfg.HostSChainNode,
 		bidderInfo:        infos,
+		requestValidator:  requestValidator,
 	}
 
 	return &exchange{
@@ -338,6 +340,11 @@ func (e *exchange) HoldAuction(ctx context.Context, r *AuctionRequest, debugLog
 		SChain: requestExt.GetSChain(),
 	}
 	bidderRequests, privacyLabels, errs := e.requestSplitter.cleanOpenRTBRequests(ctx, *r, requestExtLegacy, gdprSignal, gdprEnforced, bidAdjustmentFactors)
+	for _, err := range errs {
+		if errortypes.ReadCode(err) == errortypes.InvalidImpFirstPartyDataErrorCode {
+			return nil, err
+		}
+	}
 	errs = append(errs, floorErrs...)
 
 	mergedBidAdj, err := bidadjustment.Merge(r.BidRequestWrapper, r.Account.BidAdjustments)
diff --git a/exchange/utils.go b/exchange/utils.go
index e64ac6aa..2074d7f2 100644
--- a/exchange/utils.go
+++ b/exchange/utils.go
@@ -22,6 +22,7 @@ import (
 	"github.com/prebid/prebid-server/v2/gdpr"
 	"github.com/prebid/prebid-server/v2/metrics"
 	"github.com/prebid/prebid-server/v2/openrtb_ext"
+	// "github.com/prebid/prebid-server/v2/ortb/validation"
 	"github.com/prebid/prebid-server/v2/privacy"
 	"github.com/prebid/prebid-server/v2/privacy/ccpa"
 	"github.com/prebid/prebid-server/v2/privacy/lmt"
@@ -50,6 +51,7 @@ type requestSplitter struct {
 	gdprPermsBuilder  gdpr.PermissionsBuilder
 	hostSChainNode    *openrtb2.SupplyChainNode
 	bidderInfo        config.BidderInfos
+	requestValidator  ortb.RequestValidator
 }
 
 // cleanOpenRTBRequests splits the input request into requests which are sanitized for each bidder. Intended behavior is:
@@ -75,7 +77,9 @@ func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context,
 
 	bidderImpWithBidResp := stored_responses.InitStoredBidResponses(req.BidRequest, auctionReq.StoredBidResponses)
 
-	impsByBidder, err := splitImps(req.BidRequest.Imp)
+	hasStoredAuctionResponses := len(auctionReq.StoredAuctionResponses) > 0
+
+	impsByBidder, err := splitImps(req.BidRequest.Imp, rs.requestValidator, requestAliases, hasStoredAuctionResponses, auctionReq.StoredBidResponses)
 	if err != nil {
 		errs = []error{err}
 		return
@@ -566,7 +570,7 @@ func extractBuyerUIDs(user *openrtb2.User) (map[string]string, error) {
 // The "imp.ext" value of the rubicon Imp will only contain the "prebid" values, and "rubicon" value at the "bidder" key.
 //
 // The goal here is so that Bidders only get Imps and Imp.Ext values which are intended for them.
-func splitImps(imps []openrtb2.Imp) (map[string][]openrtb2.Imp, error) {
+func splitImps(imps []openrtb2.Imp, requestValidator ortb.RequestValidator, requestAliases map[string]string, hasStoredAuctionResponses bool, storedBidResponses stored_responses.ImpBidderStoredResp) (map[string][]openrtb2.Imp, error) {
 	bidderImps := make(map[string][]openrtb2.Imp)
 
 	for i, imp := range imps {
@@ -587,6 +591,11 @@ func splitImps(imps []openrtb2.Imp) (map[string][]openrtb2.Imp, error) {
 			jsonutil.Unmarshal(impExtPrebidBidderJSON, &impExtPrebidBidder)
 		}
 
+		var impExtPrebidImp map[string]json.RawMessage
+		if impExtPrebidImpJSON, exists := impExtPrebid["imp"]; exists {
+			jsonutil.Unmarshal(impExtPrebidImpJSON, &impExtPrebidImp)
+		}
+
 		sanitizedImpExt, err := createSanitizedImpExt(impExt, impExtPrebid)
 		if err != nil {
 			return nil, fmt.Errorf("unable to remove other bidder fields for imp[%d]: %v", i, err)
@@ -595,6 +604,18 @@ func splitImps(imps []openrtb2.Imp) (map[string][]openrtb2.Imp, error) {
 		for bidder, bidderExt := range impExtPrebidBidder {
 			impCopy := imp
 
+			if impBidderFPD, exists := impExtPrebidImp[bidder]; exists {
+				if err := mergeImpFPD(&impCopy, impBidderFPD, i); err != nil {
+					return nil, err
+				}
+				impWrapper := openrtb_ext.ImpWrapper{Imp: &impCopy}
+				if err := requestValidator.ValidateImp(&impWrapper, i, requestAliases, hasStoredAuctionResponses, storedBidResponses); err != nil {
+					return nil, &errortypes.InvalidImpFirstPartyData{
+						Message: fmt.Sprintf("merging bidder imp first party data for imp %s results in an invalid imp: %v", imp.ID, err),
+					}
+				}
+			}
+
 			sanitizedImpExt[openrtb_ext.PrebidExtBidderKey] = bidderExt
 
 			impExtJSON, err := jsonutil.Marshal(sanitizedImpExt)
@@ -610,6 +631,16 @@ func splitImps(imps []openrtb2.Imp) (map[string][]openrtb2.Imp, error) {
 	return bidderImps, nil
 }
 
+func mergeImpFPD(imp *openrtb2.Imp, fpd json.RawMessage, index int) error {
+	if err := jsonutil.MergeClone(imp, fpd); err != nil {
+		if strings.Contains(err.Error(), "invalid json on existing object") {
+			return fmt.Errorf("invalid imp ext for imp[%d]", index)
+		}
+		return fmt.Errorf("invalid first party data for imp[%d]", index)
+	}
+	return nil
+}
+
 var allowedImpExtFields = map[string]interface{}{
 	openrtb_ext.AuctionEnvironmentKey:       struct{}{},
 	openrtb_ext.FirstPartyDataExtKey:        struct{}{},
diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go
index 5f6e3545..b6cda02b 100644
--- a/openrtb_ext/bidders.go
+++ b/openrtb_ext/bidders.go
@@ -300,6 +300,28 @@ func IsBidderNameReserved(name string) bool {
 	return false
 }
 
+// IsPotentialBidder returns true if the name is not reserved witbin the imp[].ext context
+func IsPotentialBidder(name string) bool {
+	switch BidderName(name) {
+	case BidderReservedContext:
+		return false
+	case BidderReservedData:
+		return false
+	case BidderReservedGPID:
+		return false
+	case BidderReservedPrebid:
+		return false
+	case BidderReservedSKAdN:
+		return false
+	case BidderReservedTID:
+		return false
+	case BidderReservedAE:
+		return false
+	default:
+		return true
+	}
+}
+
 // Names of core bidders. These names *must* match the bidder code in Prebid.js if an adapter also exists in that
 // project. You may *not* use the name 'general' as that is reserved for general error messages nor 'context' as
 // that is reserved for first party data.
@@ -555,6 +577,8 @@ var bidderNameLookup = func() map[string]BidderName {
 	return lookup
 }()
 
+type BidderNameNormalizer func(name string) (BidderName, bool)
+
 func NormalizeBidderName(name string) (BidderName, bool) {
 	nameLower := strings.ToLower(name)
 	bidderName, exists := bidderNameLookup[nameLower]
diff --git a/openrtb_ext/imp.go b/openrtb_ext/imp.go
index e5779ed3..e02cfadc 100644
--- a/openrtb_ext/imp.go
+++ b/openrtb_ext/imp.go
@@ -46,6 +46,9 @@ type ExtImpPrebid struct {
 	Passthrough json.RawMessage `json:"passthrough,omitempty"`
 
 	Floors *ExtImpPrebidFloors `json:"floors,omitempty"`
+
+	// Imp specifies any imp bidder-specific first party data
+	Imp map[string]json.RawMessage `json:"imp,omitempty"`
 }
 
 type ExtImpDataAdServer struct {
diff --git a/ortb/request_validator.go b/ortb/request_validator.go
new file mode 100644
index 00000000..e38b8da2
--- /dev/null
+++ b/ortb/request_validator.go
@@ -0,0 +1,176 @@
+package ortb
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/prebid/prebid-server/v2/errortypes"
+	"github.com/prebid/prebid-server/v2/openrtb_ext"
+	"github.com/prebid/prebid-server/v2/stored_responses"
+)
+
+type RequestValidator interface {
+	ValidateImp(imp *openrtb_ext.ImpWrapper, index int, aliases map[string]string, hasStoredAuctionResponses bool, storedBidResponses stored_responses.ImpBidderStoredResp) []error
+}
+
+func NewRequestValidator(bidderMap map[string]openrtb_ext.BidderName, disabledBidders map[string]string, paramsValidator openrtb_ext.BidderParamValidator) RequestValidator {
+	return &standardRequestValidator{
+		bidderMap:       bidderMap,
+		disabledBidders: disabledBidders,
+		paramsValidator: paramsValidator,
+	}
+}
+
+type standardRequestValidator struct {
+	bidderMap       map[string]openrtb_ext.BidderName
+	disabledBidders map[string]string
+	paramsValidator openrtb_ext.BidderParamValidator
+}
+
+func (srv *standardRequestValidator) ValidateImp(imp *openrtb_ext.ImpWrapper, index int, aliases map[string]string, hasStoredAuctionResponses bool, storedBidResponses stored_responses.ImpBidderStoredResp) []error {
+	if imp.ID == "" {
+		return []error{fmt.Errorf("request.imp[%d] missing required field: \"id\"", index)}
+	}
+
+	if len(imp.Metric) != 0 {
+		return []error{fmt.Errorf("request.imp[%d].metric is not yet supported by prebid-server. Support may be added in the future", index)}
+	}
+
+	if imp.Banner == nil && imp.Video == nil && imp.Audio == nil && imp.Native == nil {
+		return []error{fmt.Errorf("request.imp[%d] must contain at least one of \"banner\", \"video\", \"audio\", or \"native\"", index)}
+	}
+
+	if err := validateBanner(imp.Banner, index, isInterstitial(imp)); err != nil {
+		return []error{err}
+	}
+
+	if err := validateVideo(imp.Video, index); err != nil {
+		return []error{err}
+	}
+
+	if err := validateAudio(imp.Audio, index); err != nil {
+		return []error{err}
+	}
+
+	if err := fillAndValidateNative(imp.Native, index); err != nil {
+		return []error{err}
+	}
+
+	if err := validatePmp(imp.PMP, index); err != nil {
+		return []error{err}
+	}
+
+	errL := srv.validateImpExt(imp, aliases, index, hasStoredAuctionResponses, storedBidResponses)
+	if len(errL) != 0 {
+		return errL
+	}
+
+	return nil
+}
+
+func (srv *standardRequestValidator) validateImpExt(imp *openrtb_ext.ImpWrapper, aliases map[string]string, impIndex int, hasStoredAuctionResponses bool, storedBidResp stored_responses.ImpBidderStoredResp) []error {
+	if len(imp.Ext) == 0 {
+		return []error{fmt.Errorf("request.imp[%d].ext is required", impIndex)}
+	}
+
+	impExt, err := imp.GetImpExt()
+	if err != nil {
+		return []error{err}
+	}
+
+	prebid := impExt.GetOrCreatePrebid()
+	prebidModified := false
+
+	if prebid.Bidder == nil {
+		prebid.Bidder = make(map[string]json.RawMessage)
+	}
+
+	ext := impExt.GetExt()
+	extModified := false
+
+	// promote imp[].ext.BIDDER to newer imp[].ext.prebid.bidder.BIDDER location, with the later taking precedence
+	for k, v := range ext {
+		if openrtb_ext.IsPotentialBidder(k) {
+			if _, exists := prebid.Bidder[k]; !exists {
+				prebid.Bidder[k] = v
+				prebidModified = true
+			}
+			delete(ext, k)
+			extModified = true
+		}
+	}
+
+	if hasStoredAuctionResponses && prebid.StoredAuctionResponse == nil {
+		return []error{fmt.Errorf("request validation failed. The StoredAuctionResponse.ID field must be completely present with, or completely absent from, all impressions in request. No StoredAuctionResponse data found for request.imp[%d].ext.prebid \n", impIndex)}
+	}
+
+	if err := srv.validateStoredBidResponses(prebid, storedBidResp, imp.ID); err != nil {
+		return []error{err}
+	}
+
+	errL := []error{}
+
+	for bidder, ext := range prebid.Bidder {
+		coreBidder, _ := openrtb_ext.NormalizeBidderName(bidder)
+		if tmp, isAlias := aliases[bidder]; isAlias {
+			coreBidder = openrtb_ext.BidderName(tmp)
+		}
+
+		if coreBidderNormalized, isValid := srv.bidderMap[coreBidder.String()]; isValid {
+			if err := srv.paramsValidator.Validate(coreBidderNormalized, ext); err != nil {
+				return []error{fmt.Errorf("request.imp[%d].ext.prebid.bidder.%s failed validation.\n%v", impIndex, bidder, err)}
+			}
+		} else {
+			if msg, isDisabled := srv.disabledBidders[bidder]; isDisabled {
+				errL = append(errL, &errortypes.BidderTemporarilyDisabled{Message: msg})
+				delete(prebid.Bidder, bidder)
+				prebidModified = true
+			} else {
+				return []error{fmt.Errorf("request.imp[%d].ext.prebid.bidder contains unknown bidder: %s. Did you forget an alias in request.ext.prebid.aliases?", impIndex, bidder)}
+			}
+		}
+	}
+
+	if len(prebid.Bidder) == 0 {
+		errL = append(errL, fmt.Errorf("request.imp[%d].ext.prebid.bidder must contain at least one bidder", impIndex))
+		return errL
+	}
+
+	if prebidModified {
+		impExt.SetPrebid(prebid)
+	}
+	if extModified {
+		impExt.SetExt(ext)
+	}
+
+	return errL
+}
+
+func (srv *standardRequestValidator) validateStoredBidResponses(prebid *openrtb_ext.ExtImpPrebid, storedBidResp stored_responses.ImpBidderStoredResp, impId string) error {
+	if storedBidResp == nil && len(prebid.StoredBidResponse) == 0 {
+		return nil
+	}
+
+	if storedBidResp == nil {
+		return generateStoredBidResponseValidationError(impId)
+	}
+	if bidResponses, ok := storedBidResp[impId]; ok {
+		if len(bidResponses) != len(prebid.Bidder) {
+			return generateStoredBidResponseValidationError(impId)
+		}
+
+		for bidderName := range bidResponses {
+			if _, bidderNameOk := openrtb_ext.NormalizeBidderName(bidderName); !bidderNameOk {
+				return fmt.Errorf(`unrecognized bidder "%v"`, bidderName)
+			}
+			if _, present := prebid.Bidder[bidderName]; !present {
+				return generateStoredBidResponseValidationError(impId)
+			}
+		}
+	}
+	return nil
+}
+
+func generateStoredBidResponseValidationError(impID string) error {
+	return fmt.Errorf("request validation failed. Stored bid responses are specified for imp %s. Bidders specified in imp.ext should match with bidders specified in imp.ext.prebid.storedbidresponse", impID)
+}
diff --git a/ortb/request_validator_audio.go b/ortb/request_validator_audio.go
new file mode 100644
index 00000000..f1ed46f5
--- /dev/null
+++ b/ortb/request_validator_audio.go
@@ -0,0 +1,34 @@
+package ortb
+
+import (
+	"fmt"
+
+	"github.com/prebid/openrtb/v20/openrtb2"
+)
+
+func validateAudio(audio *openrtb2.Audio, impIndex int) error {
+	if audio == nil {
+		return nil
+	}
+
+	if len(audio.MIMEs) < 1 {
+		return fmt.Errorf("request.imp[%d].audio.mimes must contain at least one supported MIME type", impIndex)
+	}
+
+	// The following fields were previously uints in the OpenRTB library we use, but have
+	// since been changed to ints. We decided to maintain the non-negative check.
+	if audio.Sequence < 0 {
+		return fmt.Errorf("request.imp[%d].audio.sequence must be a positive number", impIndex)
+	}
+	if audio.MaxSeq < 0 {
+		return fmt.Errorf("request.imp[%d].audio.maxseq must be a positive number", impIndex)
+	}
+	if audio.MinBitrate < 0 {
+		return fmt.Errorf("request.imp[%d].audio.minbitrate must be a positive number", impIndex)
+	}
+	if audio.MaxBitrate < 0 {
+		return fmt.Errorf("request.imp[%d].audio.maxbitrate must be a positive number", impIndex)
+	}
+
+	return nil
+}
diff --git a/ortb/request_validator_banner.go b/ortb/request_validator_banner.go
new file mode 100644
index 00000000..55197c56
--- /dev/null
+++ b/ortb/request_validator_banner.go
@@ -0,0 +1,95 @@
+package ortb
+
+import (
+	"fmt"
+
+	"github.com/prebid/openrtb/v20/openrtb2"
+	"github.com/prebid/prebid-server/v2/openrtb_ext"
+)
+
+func isInterstitial(imp *openrtb_ext.ImpWrapper) bool {
+	return imp.Instl == 1
+}
+
+func validateBanner(banner *openrtb2.Banner, impIndex int, isInterstitial bool) error {
+	if banner == nil {
+		return nil
+	}
+
+	// The following fields were previously uints in the OpenRTB library we use, but have
+	// since been changed to ints. We decided to maintain the non-negative check.
+	if banner.W != nil && *banner.W < 0 {
+		return fmt.Errorf("request.imp[%d].banner.w must be a positive number", impIndex)
+	}
+	if banner.H != nil && *banner.H < 0 {
+		return fmt.Errorf("request.imp[%d].banner.h must be a positive number", impIndex)
+	}
+
+	// The following fields are deprecated in the OpenRTB 2.5 spec but are still present
+	// in the OpenRTB library we use. Enforce they are not specified.
+	if banner.WMin != 0 {
+		return fmt.Errorf("request.imp[%d].banner uses unsupported property: \"wmin\". Use the \"format\" array instead.", impIndex)
+	}
+	if banner.WMax != 0 {
+		return fmt.Errorf("request.imp[%d].banner uses unsupported property: \"wmax\". Use the \"format\" array instead.", impIndex)
+	}
+	if banner.HMin != 0 {
+		return fmt.Errorf("request.imp[%d].banner uses unsupported property: \"hmin\". Use the \"format\" array instead.", impIndex)
+	}
+	if banner.HMax != 0 {
+		return fmt.Errorf("request.imp[%d].banner uses unsupported property: \"hmax\". Use the \"format\" array instead.", impIndex)
+	}
+
+	hasRootSize := banner.H != nil && banner.W != nil && *banner.H > 0 && *banner.W > 0
+	if !hasRootSize && len(banner.Format) == 0 && !isInterstitial {
+		return fmt.Errorf("request.imp[%d].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements.", impIndex)
+	}
+
+	for i, format := range banner.Format {
+		if err := validateFormat(&format, impIndex, i); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func validateFormat(format *openrtb2.Format, impIndex, formatIndex int) error {
+	if format == nil {
+		return nil
+	}
+	usesHW := format.W != 0 || format.H != 0
+	usesRatios := format.WMin != 0 || format.WRatio != 0 || format.HRatio != 0
+
+	// The following fields were previously uints in the OpenRTB library we use, but have
+	// since been changed to ints. We decided to maintain the non-negative check.
+	if format.W < 0 {
+		return fmt.Errorf("request.imp[%d].banner.format[%d].w must be a positive number", impIndex, formatIndex)
+	}
+	if format.H < 0 {
+		return fmt.Errorf("request.imp[%d].banner.format[%d].h must be a positive number", impIndex, formatIndex)
+	}
+	if format.WRatio < 0 {
+		return fmt.Errorf("request.imp[%d].banner.format[%d].wratio must be a positive number", impIndex, formatIndex)
+	}
+	if format.HRatio < 0 {
+		return fmt.Errorf("request.imp[%d].banner.format[%d].hratio must be a positive number", impIndex, formatIndex)
+	}
+	if format.WMin < 0 {
+		return fmt.Errorf("request.imp[%d].banner.format[%d].wmin must be a positive number", impIndex, formatIndex)
+	}
+
+	if usesHW && usesRatios {
+		return fmt.Errorf("Request imp[%d].banner.format[%d] should define *either* {w, h} *or* {wmin, wratio, hratio}, but not both. If both are valid, send two \"format\" objects in the request.", impIndex, formatIndex)
+	}
+	if !usesHW && !usesRatios {
+		return fmt.Errorf("Request imp[%d].banner.format[%d] should define *either* {w, h} (for static size requirements) *or* {wmin, wratio, hratio} (for flexible sizes) to be non-zero.", impIndex, formatIndex)
+	}
+	if usesHW && (format.W == 0 || format.H == 0) {
+		return fmt.Errorf("Request imp[%d].banner.format[%d] must define non-zero \"h\" and \"w\" properties.", impIndex, formatIndex)
+	}
+	if usesRatios && (format.WMin == 0 || format.WRatio == 0 || format.HRatio == 0) {
+		return fmt.Errorf("Request imp[%d].banner.format[%d] must define non-zero \"wmin\", \"wratio\", and \"hratio\" properties.", impIndex, formatIndex)
+	}
+	return nil
+}
diff --git a/ortb/request_validator_native.go b/ortb/request_validator_native.go
new file mode 100644
index 00000000..02c0c606
--- /dev/null
+++ b/ortb/request_validator_native.go
@@ -0,0 +1,273 @@
+package ortb
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/prebid/openrtb/v20/adcom1"
+	"github.com/prebid/openrtb/v20/native1"
+	nativeRequests "github.com/prebid/openrtb/v20/native1/request"
+	"github.com/prebid/openrtb/v20/openrtb2"
+	"github.com/prebid/prebid-server/v2/openrtb_ext"
+	"github.com/prebid/prebid-server/v2/util/jsonutil"
+)
+
+// fillAndValidateNative validates the request, and assigns the Asset IDs as recommended by the Native v1.2 spec.
+func fillAndValidateNative(n *openrtb2.Native, impIndex int) error {
+	if n == nil {
+		return nil
+	}
+
+	if len(n.Request) == 0 {
+		return fmt.Errorf("request.imp[%d].native missing required property \"request\"", impIndex)
+	}
+	var nativePayload nativeRequests.Request
+	if err := jsonutil.UnmarshalValid(json.RawMessage(n.Request), &nativePayload); err != nil {
+		return err
+	}
+
+	if err := validateNativeContextTypes(nativePayload.Context, nativePayload.ContextSubType, impIndex); err != nil {
+		return err
+	}
+	if err := validateNativePlacementType(nativePayload.PlcmtType, impIndex); err != nil {
+		return err
+	}
+	if err := fillAndValidateNativeAssets(nativePayload.Assets, impIndex); err != nil {
+		return err
+	}
+	if err := validateNativeEventTrackers(nativePayload.EventTrackers, impIndex); err != nil {
+		return err
+	}
+
+	serialized, err := jsonutil.Marshal(nativePayload)
+	if err != nil {
+		return err
+	}
+	n.Request = string(serialized)
+	return nil
+}
+
+func validateNativeContextTypes(cType native1.ContextType, cSubtype native1.ContextSubType, impIndex int) error {
+	if cType == 0 {
+		// Context is only recommended, so none is a valid type.
+		return nil
+	}
+	if cType < native1.ContextTypeContent || (cType > native1.ContextTypeProduct && cType < openrtb_ext.NativeExchangeSpecificLowerBound) {
+		return fmt.Errorf("request.imp[%d].native.request.context is invalid. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39", impIndex)
+	}
+	if cSubtype < 0 {
+		return fmt.Errorf("request.imp[%d].native.request.contextsubtype value can't be less than 0. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39", impIndex)
+	}
+	if cSubtype == 0 {
+		return nil
+	}
+	if cSubtype >= native1.ContextSubTypeGeneral && cSubtype <= native1.ContextSubTypeUserGenerated {
+		if cType != native1.ContextTypeContent && cType < openrtb_ext.NativeExchangeSpecificLowerBound {
+			return fmt.Errorf("request.imp[%d].native.request.context is %d, but contextsubtype is %d. This is an invalid combination. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39", impIndex, cType, cSubtype)
+		}
+		return nil
+	}
+	if cSubtype >= native1.ContextSubTypeSocial && cSubtype <= native1.ContextSubTypeChat {
+		if cType != native1.ContextTypeSocial && cType < openrtb_ext.NativeExchangeSpecificLowerBound {
+			return fmt.Errorf("request.imp[%d].native.request.context is %d, but contextsubtype is %d. This is an invalid combination. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39", impIndex, cType, cSubtype)
+		}
+		return nil
+	}
+	if cSubtype >= native1.ContextSubTypeSelling && cSubtype <= native1.ContextSubTypeProductReview {
+		if cType != native1.ContextTypeProduct && cType < openrtb_ext.NativeExchangeSpecificLowerBound {
+			return fmt.Errorf("request.imp[%d].native.request.context is %d, but contextsubtype is %d. This is an invalid combination. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39", impIndex, cType, cSubtype)
+		}
+		return nil
+	}
+	if cSubtype >= openrtb_ext.NativeExchangeSpecificLowerBound {
+		return nil
+	}
+
+	return fmt.Errorf("request.imp[%d].native.request.contextsubtype is invalid. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39", impIndex)
+}
+
+func validateNativePlacementType(pt native1.PlacementType, impIndex int) error {
+	if pt == 0 {
+		// Placement Type is only recommended, not required.
+		return nil
+	}
+	if pt < native1.PlacementTypeFeed || (pt > native1.PlacementTypeRecommendationWidget && pt < openrtb_ext.NativeExchangeSpecificLowerBound) {
+		return fmt.Errorf("request.imp[%d].native.request.plcmttype is invalid. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=40", impIndex)
+	}
+	return nil
+}
+
+func fillAndValidateNativeAssets(assets []nativeRequests.Asset, impIndex int) error {
+	if len(assets) < 1 {
+		return fmt.Errorf("request.imp[%d].native.request.assets must be an array containing at least one object", impIndex)
+	}
+
+	assetIDs := make(map[int64]struct{}, len(assets))
+
+	// If none of the asset IDs are defined by the caller, then prebid server should assign its own unique IDs. But
+	// if the caller did assign its own asset IDs, then prebid server will respect those IDs
+	assignAssetIDs := true
+	for i := 0; i < len(assets); i++ {
+		assignAssetIDs = assignAssetIDs && (assets[i].ID == 0)
+	}
+
+	for i := 0; i < len(assets); i++ {
+		if err := validateNativeAsset(assets[i], impIndex, i); err != nil {
+			return err
+		}
+
+		if assignAssetIDs {
+			assets[i].ID = int64(i)
+			continue
+		}
+
+		// Each asset should have a unique ID thats assigned by the caller
+		if _, ok := assetIDs[assets[i].ID]; ok {
+			return fmt.Errorf("request.imp[%d].native.request.assets[%d].id is already being used by another asset. Each asset ID must be unique.", impIndex, i)
+		}
+
+		assetIDs[assets[i].ID] = struct{}{}
+	}
+
+	return nil
+}
+
+func validateNativeAsset(asset nativeRequests.Asset, impIndex int, assetIndex int) error {
+	assetErr := "request.imp[%d].native.request.assets[%d] must define exactly one of {title, img, video, data}"
+	foundType := false
+
+	if asset.Title != nil {
+		foundType = true
+		if err := validateNativeAssetTitle(asset.Title, impIndex, assetIndex); err != nil {
+			return err
+		}
+	}
+
+	if asset.Img != nil {
+		if foundType {
+			return fmt.Errorf(assetErr, impIndex, assetIndex)
+		}
+		foundType = true
+		if err := validateNativeAssetImage(asset.Img, impIndex, assetIndex); err != nil {
+			return err
+		}
+	}
+
+	if asset.Video != nil {
+		if foundType {
+			return fmt.Errorf(assetErr, impIndex, assetIndex)
+		}
+		foundType = true
+		if err := validateNativeAssetVideo(asset.Video, impIndex, assetIndex); err != nil {
+			return err
+		}
+	}
+
+	if asset.Data != nil {
+		if foundType {
+			return fmt.Errorf(assetErr, impIndex, assetIndex)
+		}
+		foundType = true
+		if err := validateNativeAssetData(asset.Data, impIndex, assetIndex); err != nil {
+			return err
+		}
+	}
+
+	if !foundType {
+		return fmt.Errorf(assetErr, impIndex, assetIndex)
+	}
+
+	return nil
+}
+
+func validateNativeEventTrackers(trackers []nativeRequests.EventTracker, impIndex int) error {
+	for i := 0; i < len(trackers); i++ {
+		if err := validateNativeEventTracker(trackers[i], impIndex, i); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func validateNativeAssetTitle(title *nativeRequests.Title, impIndex int, assetIndex int) error {
+	if title.Len < 1 {
+		return fmt.Errorf("request.imp[%d].native.request.assets[%d].title.len must be a positive number", impIndex, assetIndex)
+	}
+	return nil
+}
+
+func validateNativeEventTracker(tracker nativeRequests.EventTracker, impIndex int, eventIndex int) error {
+	if tracker.Event < native1.EventTypeImpression || (tracker.Event > native1.EventTypeViewableVideo50 && tracker.Event < openrtb_ext.NativeExchangeSpecificLowerBound) {
+		return fmt.Errorf("request.imp[%d].native.request.eventtrackers[%d].event is invalid. See section 7.6: https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=43", impIndex, eventIndex)
+	}
+	if len(tracker.Methods) < 1 {
+		return fmt.Errorf("request.imp[%d].native.request.eventtrackers[%d].method is required. See section 7.7: https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=43", impIndex, eventIndex)
+	}
+	for methodIndex, method := range tracker.Methods {
+		if method < native1.EventTrackingMethodImage || (method > native1.EventTrackingMethodJS && method < openrtb_ext.NativeExchangeSpecificLowerBound) {
+			return fmt.Errorf("request.imp[%d].native.request.eventtrackers[%d].methods[%d] is invalid. See section 7.7: https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=43", impIndex, eventIndex, methodIndex)
+		}
+	}
+
+	return nil
+}
+
+func validateNativeAssetImage(img *nativeRequests.Image, impIndex int, assetIndex int) error {
+	if img.W < 0 {
+		return fmt.Errorf("request.imp[%d].native.request.assets[%d].img.w must be a positive integer", impIndex, assetIndex)
+	}
+	if img.H < 0 {
+		return fmt.Errorf("request.imp[%d].native.request.assets[%d].img.h must be a positive integer", impIndex, assetIndex)
+	}
+	if img.WMin < 0 {
+		return fmt.Errorf("request.imp[%d].native.request.assets[%d].img.wmin must be a positive integer", impIndex, assetIndex)
+	}
+	if img.HMin < 0 {
+		return fmt.Errorf("request.imp[%d].native.request.assets[%d].img.hmin must be a positive integer", impIndex, assetIndex)
+	}
+	return nil
+}
+
+func validateNativeAssetVideo(video *nativeRequests.Video, impIndex int, assetIndex int) error {
+	if len(video.MIMEs) < 1 {
+		return fmt.Errorf("request.imp[%d].native.request.assets[%d].video.mimes must be an array with at least one MIME type", impIndex, assetIndex)
+	}
+	if video.MinDuration < 1 {
+		return fmt.Errorf("request.imp[%d].native.request.assets[%d].video.minduration must be a positive integer", impIndex, assetIndex)
+	}
+	if video.MaxDuration < 1 {
+		return fmt.Errorf("request.imp[%d].native.request.assets[%d].video.maxduration must be a positive integer", impIndex, assetIndex)
+	}
+	if err := validateNativeVideoProtocols(video.Protocols, impIndex, assetIndex); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func validateNativeAssetData(data *nativeRequests.Data, impIndex int, assetIndex int) error {
+	if data.Type < native1.DataAssetTypeSponsored || (data.Type > native1.DataAssetTypeCTAText && data.Type < 500) {
+		return fmt.Errorf("request.imp[%d].native.request.assets[%d].data.type is invalid. See section 7.4: https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=40", impIndex, assetIndex)
+	}
+
+	return nil
+}
+
+func validateNativeVideoProtocols(protocols []adcom1.MediaCreativeSubtype, impIndex int, assetIndex int) error {
+	if len(protocols) < 1 {
+		return fmt.Errorf("request.imp[%d].native.request.assets[%d].video.protocols must be an array with at least one element", impIndex, assetIndex)
+	}
+	for i := 0; i < len(protocols); i++ {
+		if err := validateNativeVideoProtocol(protocols[i], impIndex, assetIndex, i); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func validateNativeVideoProtocol(protocol adcom1.MediaCreativeSubtype, impIndex int, assetIndex int, protocolIndex int) error {
+	if protocol < adcom1.CreativeVAST10 || protocol > adcom1.CreativeDAAST10Wrapper {
+		return fmt.Errorf("request.imp[%d].native.request.assets[%d].video.protocols[%d] is invalid. See Section 5.8: https://www.iab.com/wp-content/uploads/2016/03/OpenRTB-API-Specification-Version-2-5-FINAL.pdf#page=52", impIndex, assetIndex, protocolIndex)
+	}
+	return nil
+}
diff --git a/ortb/request_validator_pmp.go b/ortb/request_validator_pmp.go
new file mode 100644
index 00000000..ca36566d
--- /dev/null
+++ b/ortb/request_validator_pmp.go
@@ -0,0 +1,20 @@
+package ortb
+
+import (
+	"fmt"
+
+	"github.com/prebid/openrtb/v20/openrtb2"
+)
+
+func validatePmp(pmp *openrtb2.PMP, impIndex int) error {
+	if pmp == nil {
+		return nil
+	}
+
+	for dealIndex, deal := range pmp.Deals {
+		if deal.ID == "" {
+			return fmt.Errorf("request.imp[%d].pmp.deals[%d] missing required field: \"id\"", impIndex, dealIndex)
+		}
+	}
+	return nil
+}
diff --git a/ortb/request_validator_video.go b/ortb/request_validator_video.go
new file mode 100644
index 00000000..4674d0c4
--- /dev/null
+++ b/ortb/request_validator_video.go
@@ -0,0 +1,34 @@
+package ortb
+
+import (
+	"fmt"
+
+	"github.com/prebid/openrtb/v20/openrtb2"
+)
+
+func validateVideo(video *openrtb2.Video, impIndex int) error {
+	if video == nil {
+		return nil
+	}
+
+	if len(video.MIMEs) < 1 {
+		return fmt.Errorf("request.imp[%d].video.mimes must contain at least one supported MIME type", impIndex)
+	}
+
+	// The following fields were previously uints in the OpenRTB library we use, but have
+	// since been changed to ints. We decided to maintain the non-negative check.
+	if video.W != nil && *video.W < 0 {
+		return fmt.Errorf("request.imp[%d].video.w must be a positive number", impIndex)
+	}
+	if video.H != nil && *video.H < 0 {
+		return fmt.Errorf("request.imp[%d].video.h must be a positive number", impIndex)
+	}
+	if video.MinBitRate < 0 {
+		return fmt.Errorf("request.imp[%d].video.minbitrate must be a positive number", impIndex)
+	}
+	if video.MaxBitRate < 0 {
+		return fmt.Errorf("request.imp[%d].video.maxbitrate must be a positive number", impIndex)
+	}
+
+	return nil
+}
diff --git a/router/router.go b/router/router.go
index c285bbda..c4020383 100644
--- a/router/router.go
+++ b/router/router.go
@@ -29,6 +29,7 @@ import (
 	"github.com/prebid/prebid-server/v2/modules"
 	"github.com/prebid/prebid-server/v2/modules/moduledeps"
 	"github.com/prebid/prebid-server/v2/openrtb_ext"
+	"github.com/prebid/prebid-server/v2/ortb"
 	"github.com/prebid/prebid-server/v2/pbs"
 	pbc "github.com/prebid/prebid-server/v2/prebid_cache_client"
 	"github.com/prebid/prebid-server/v2/router/aspects"
@@ -235,24 +236,25 @@ func New(cfg *config.Configuration, rateConvertor *currency.RateConverter) (r *R
 		glog.Fatalf("Failed to create ads cert signer: %v", err)
 	}
 
+	requestValidator := ortb.NewRequestValidator(activeBidders, disabledBidders, paramsValidator)
 	priceFloorFetcher := floors.NewPriceFloorFetcher(cfg.PriceFloors, floorFechterHttpClient, r.MetricsEngine)
 
 	tmaxAdjustments := exchange.ProcessTMaxAdjustments(cfg.TmaxAdjustments)
 	planBuilder := hooks.NewExecutionPlanBuilder(cfg.Hooks, repo)
 	macroReplacer := macros.NewStringIndexBasedReplacer()
-	theExchange := exchange.NewExchange(adapters, cacheClient, cfg, syncersByBidder, r.MetricsEngine, cfg.BidderInfos, gdprPermsBuilder, rateConvertor, categoriesFetcher, adsCertSigner, macroReplacer, priceFloorFetcher)
+	theExchange := exchange.NewExchange(adapters, cacheClient, cfg, requestValidator, syncersByBidder, r.MetricsEngine, cfg.BidderInfos, gdprPermsBuilder, rateConvertor, categoriesFetcher, adsCertSigner, macroReplacer, priceFloorFetcher)
 	var uuidGenerator uuidutil.UUIDRandomGenerator
-	openrtbEndpoint, err := openrtb2.NewEndpoint(uuidGenerator, theExchange, paramsValidator, fetcher, accounts, cfg, r.MetricsEngine, analyticsRunner, disabledBidders, defReqJSON, activeBidders, storedRespFetcher, planBuilder, tmaxAdjustments)
+	openrtbEndpoint, err := openrtb2.NewEndpoint(uuidGenerator, theExchange, requestValidator, fetcher, accounts, cfg, r.MetricsEngine, analyticsRunner, disabledBidders, defReqJSON, activeBidders, storedRespFetcher, planBuilder, tmaxAdjustments)
 	if err != nil {
 		glog.Fatalf("Failed to create the openrtb2 endpoint handler. %v", err)
 	}
 
-	ampEndpoint, err := openrtb2.NewAmpEndpoint(uuidGenerator, theExchange, paramsValidator, ampFetcher, accounts, cfg, r.MetricsEngine, analyticsRunner, disabledBidders, defReqJSON, activeBidders, storedRespFetcher, planBuilder, tmaxAdjustments)
+	ampEndpoint, err := openrtb2.NewAmpEndpoint(uuidGenerator, theExchange, requestValidator, ampFetcher, accounts, cfg, r.MetricsEngine, analyticsRunner, disabledBidders, defReqJSON, activeBidders, storedRespFetcher, planBuilder, tmaxAdjustments)
 	if err != nil {
 		glog.Fatalf("Failed to create the amp endpoint handler. %v", err)
 	}
 
-	videoEndpoint, err := openrtb2.NewVideoEndpoint(uuidGenerator, theExchange, paramsValidator, fetcher, videoFetcher, accounts, cfg, r.MetricsEngine, analyticsRunner, disabledBidders, defReqJSON, activeBidders, cacheClient, tmaxAdjustments)
+	videoEndpoint, err := openrtb2.NewVideoEndpoint(uuidGenerator, theExchange, requestValidator, fetcher, videoFetcher, accounts, cfg, r.MetricsEngine, analyticsRunner, disabledBidders, defReqJSON, activeBidders, cacheClient, tmaxAdjustments)
 	if err != nil {
 		glog.Fatalf("Failed to create the video endpoint handler. %v", err)
 	}
diff --git a/static/bidder-info/tredio.yaml b/static/bidder-info/tredio.yaml
new file mode 100644
index 00000000..5ed85a93
--- /dev/null
+++ b/static/bidder-info/tredio.yaml
@@ -0,0 +1,5 @@
+endpoint: "http://tredio-prebid.smart-hub.io/pbserver/?seat={{.AccountID}}&token={{.SourceId}}"
+aliasOf: "smarthub"
+userSync:
+  supports:
+    - redirect
\ No newline at end of file
diff --git a/util/jsonutil/merge.go b/util/jsonutil/merge.go
index a104cf95..e1d69cf1 100644
--- a/util/jsonutil/merge.go
+++ b/util/jsonutil/merge.go
@@ -11,7 +11,6 @@ import (
 	jsonpatch "gopkg.in/evanphx/json-patch.v4"
 
 	"github.com/prebid/prebid-server/v2/errortypes"
-	"github.com/prebid/prebid-server/v2/util/reflectutil"
 )
 
 // jsonConfigMergeClone uses the same configuration as the `ConfigCompatibleWithStandardLibrary` profile
@@ -98,20 +97,14 @@ type sliceCloneDecoder struct {
 }
 
 func (d *sliceCloneDecoder) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
-	// don't clone if field is being set to nil. checking for nil "consumes" the null
-	// token, so must be handled in this decoder.
+	// clear the field. a new slice will be created by the original decoder if needed.
+	d.sliceType.UnsafeSetNil(ptr)
+
+	// checking for nil "consumes" the null token, so must be handled in this decoder.
 	if iter.ReadNil() {
-		d.sliceType.UnsafeSetNil(ptr)
 		return
 	}
 
-	// clone if there is an existing object. creation of new objects is handled by the
-	// original decoder.
-	if !d.sliceType.UnsafeIsNil(ptr) {
-		clone := reflectutil.UnsafeSliceClone(ptr, d.sliceType)
-		d.sliceType.UnsafeSet(ptr, clone)
-	}
-
 	d.valueDecoder.Decode(ptr, iter)
 }
 
diff --git a/util/reflectutil/slice.go b/util/reflectutil/slice.go
deleted file mode 100644
index 6da1aed2..00000000
--- a/util/reflectutil/slice.go
+++ /dev/null
@@ -1,49 +0,0 @@
-package reflectutil
-
-import (
-	"unsafe"
-
-	"github.com/modern-go/reflect2"
-)
-
-// UnsafeSliceClone clones an existing slice using unsafe.Pointer conventions. Intended
-// for use by json iterator extensions and should likely be used no where else. Nil
-// behavior is undefined as checks are expected upstream.
-func UnsafeSliceClone(ptr unsafe.Pointer, sliceType reflect2.SliceType) unsafe.Pointer {
-	// it's also possible to use `sliceType.Elem().RType`, but that returns a `uintptr`
-	// which causes `go vet` to emit a warning even though the usage is safe. this approach
-	// of copying some internals from the reflect2 package avoids the cast of `uintptr` to
-	// `unsafe.Pointer` which keeps `go vet` output clean.
-	elemRType := unpackEFace(sliceType.Elem().Type1()).data
-
-	header := (*sliceHeader)(ptr)
-	newHeader := (*sliceHeader)(sliceType.UnsafeMakeSlice(header.Len, header.Cap))
-	typedslicecopy(elemRType, *newHeader, *header)
-	return unsafe.Pointer(newHeader)
-}
-
-// sliceHeader is copied from the reflect2 package v1.0.2.
-type sliceHeader struct {
-	Data unsafe.Pointer
-	Len  int
-	Cap  int
-}
-
-// typedslicecopyis copied from the reflect2 package v1.0.2.
-// it copies a slice of elemType values from src to dst,
-// returning the number of elements copied.
-//
-//go:linkname typedslicecopy reflect.typedslicecopy
-//go:noescape
-func typedslicecopy(elemType unsafe.Pointer, dst, src sliceHeader) int
-
-// eface is copied from the reflect2 package v1.0.2.
-type eface struct {
-	rtype unsafe.Pointer
-	data  unsafe.Pointer
-}
-
-// unpackEFace is copied from the reflect2 package v1.0.2.
-func unpackEFace(obj interface{}) *eface {
-	return (*eface)(unsafe.Pointer(&obj))
-}


Test patch:
diff --git a/adapters/yieldmo/yieldmotest/exemplary/valid_currency_conversion.json b/adapters/yieldmo/yieldmotest/exemplary/valid_currency_conversion.json
new file mode 100644
index 00000000..15b4aa3a
--- /dev/null
+++ b/adapters/yieldmo/yieldmotest/exemplary/valid_currency_conversion.json
@@ -0,0 +1,150 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "bidfloor": 1.0,
+        "bidfloorcur": "EUR",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "placementId": "123"
+          }
+        }
+      }
+    ],
+    "site": {
+      "id": "fake-site-id"
+    },
+    "regs": {
+      "coppa": 1,
+      "ext": {
+        "gdpr": 1,
+        "us_privacy": "uspConsentString",
+        "gpp": "gppString",
+        "gpp_sid": [6]
+      }
+    },
+    "ext": {
+      "prebid": {
+        "currency": {
+          "rates": {
+            "EUR": {
+              "USD": 1.1
+            }
+          }
+        }
+      }
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://ads.yieldmo.com/openrtb2",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "bidfloor": 1.1,
+              "bidfloorcur": "USD",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 250
+                  }
+                ]
+              },
+              "ext": {
+                "placement_id": "123"
+              }
+            }
+          ],
+          "site": {
+            "id": "fake-site-id"
+          },
+          "regs": {
+            "coppa": 1,
+            "ext": {
+              "gdpr": 1,
+              "us_privacy": "uspConsentString",
+              "gpp": "gppString",
+              "gpp_sid": [6]
+            }
+          },
+          "ext": {
+            "prebid": {
+              "currency": {
+                "rates": {
+                  "EUR": {
+                    "USD": 1.1
+                  }
+                }
+              }
+            }
+          }
+        },
+        "impIDs":["test-imp-id"]
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "seat": "yieldmo",
+              "bid": [
+                {
+                  "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800",
+                  "impid": "test-imp-id",
+                  "price": 1.500000,
+                  "adm": "some-test-ad",
+                  "crid": "crid_10",
+                  "h": 250,
+                  "w": 300,
+                  "ext":
+                    {
+                      "mediatype": "banner"
+                    }
+                }
+              ]
+            }
+          ],
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800",
+            "impid": "test-imp-id",
+            "price": 1.5,
+            "adm": "some-test-ad",
+            "crid": "crid_10",
+            "w": 300,
+            "h": 250,
+            "ext":
+            {
+              "mediatype": "banner"
+            }
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/yieldmo/yieldmotest/supplemental/unsupported_currency.json b/adapters/yieldmo/yieldmotest/supplemental/unsupported_currency.json
new file mode 100644
index 00000000..a6d108aa
--- /dev/null
+++ b/adapters/yieldmo/yieldmotest/supplemental/unsupported_currency.json
@@ -0,0 +1,109 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "bidfloor": 1.0,
+        "bidfloorcur": "EUR",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "placementId": "123"
+          }
+        }
+      }
+    ],
+    "site": {
+      "id": "fake-site-id"
+    },
+    "regs": {
+      "coppa": 1,
+      "ext": {
+        "gdpr": 1,
+        "us_privacy": "uspConsentString",
+        "gpp": "gppString",
+        "gpp_sid": [6]
+      }
+    },
+    "ext": {
+      "prebid": {
+        "currency": {
+          "rates": {
+            "EUR": {
+              "GBP": 0.85
+            }
+          }
+        }
+      }
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://ads.yieldmo.com/openrtb2",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "bidfloor": 1.0,
+              "bidfloorcur": "EUR",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 250
+                  }
+                ]
+              },
+              "ext": {
+                "placement_id": "123"
+              }
+            }
+          ],
+          "site": {
+            "id": "fake-site-id"
+          },
+          "regs": {
+            "coppa": 1,
+            "ext": {
+              "gdpr": 1,
+              "us_privacy": "uspConsentString",
+              "gpp": "gppString",
+              "gpp_sid": [6]
+            }
+          },
+          "ext": {
+            "prebid": {
+              "currency": {
+                "rates": {
+                  "EUR": {
+                    "GBP": 0.85
+                  }
+                }
+              }
+            }
+          }
+        },
+        "impIDs":["test-imp-id"]
+      },
+      "mockResponse": {
+        "status": 204
+      }
+    }
+  ],
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "Unable to convert provided bid floor currency from EUR to USD",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/endpoints/events/vtrack_test.go b/endpoints/events/vtrack_test.go
index 3ccfeacf..695ba87a 100644
--- a/endpoints/events/vtrack_test.go
+++ b/endpoints/events/vtrack_test.go
@@ -471,7 +471,7 @@ func TestShouldSendToCacheExpectedPutsAndUpdatableBiddersWhenBidderVastAllowed(t
 
 	recorder := httptest.NewRecorder()
 
-	var mockNormalizeBidderName normalizeBidderName = func(name string) (openrtb_ext.BidderName, bool) {
+	var mockNormalizeBidderName openrtb_ext.BidderNameNormalizer = func(name string) (openrtb_ext.BidderName, bool) {
 		return openrtb_ext.BidderName(name), true
 	}
 	e := vtrackEndpoint{
diff --git a/endpoints/openrtb2/amp_auction_test.go b/endpoints/openrtb2/amp_auction_test.go
index 6e6e1ac8..e9b0015a 100644
--- a/endpoints/openrtb2/amp_auction_test.go
+++ b/endpoints/openrtb2/amp_auction_test.go
@@ -31,6 +31,7 @@ import (
 	"github.com/prebid/prebid-server/v2/metrics"
 	metricsConfig "github.com/prebid/prebid-server/v2/metrics/config"
 	"github.com/prebid/prebid-server/v2/openrtb_ext"
+	"github.com/prebid/prebid-server/v2/ortb"
 	"github.com/prebid/prebid-server/v2/privacy"
 	"github.com/prebid/prebid-server/v2/stored_requests/backends/empty_fetcher"
 	"github.com/prebid/prebid-server/v2/util/jsonutil"
@@ -202,7 +203,7 @@ func TestAMPPageInfo(t *testing.T) {
 	endpoint, _ := NewAmpEndpoint(
 		fakeUUIDGenerator{},
 		exchange,
-		newParamsValidator(t),
+		ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, newParamsValidator(t)),
 		&mockAmpStoredReqFetcher{stored},
 		empty_fetcher.EmptyFetcher{},
 		&config.Configuration{MaxRequestSize: maxSize},
@@ -300,10 +301,11 @@ func TestGDPRConsent(t *testing.T) {
 
 		// Build Exchange Endpoint
 		mockExchange := &mockAmpExchange{}
+
 		endpoint, _ := NewAmpEndpoint(
 			fakeUUIDGenerator{},
 			mockExchange,
-			newParamsValidator(t),
+			ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, newParamsValidator(t)),
 			&mockAmpStoredReqFetcher{stored},
 			empty_fetcher.EmptyFetcher{},
 			&config.Configuration{
@@ -730,7 +732,7 @@ func TestCCPAConsent(t *testing.T) {
 		endpoint, _ := NewAmpEndpoint(
 			fakeUUIDGenerator{},
 			mockExchange,
-			newParamsValidator(t),
+			ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, newParamsValidator(t)),
 			&mockAmpStoredReqFetcher{stored},
 			empty_fetcher.EmptyFetcher{},
 			&config.Configuration{MaxRequestSize: maxSize},
@@ -844,7 +846,7 @@ func TestConsentWarnings(t *testing.T) {
 		endpoint, _ := NewAmpEndpoint(
 			fakeUUIDGenerator{},
 			mockExchange,
-			newParamsValidator(t),
+			ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, newParamsValidator(t)),
 			&mockAmpStoredReqFetcher{stored},
 			empty_fetcher.EmptyFetcher{},
 			&config.Configuration{MaxRequestSize: maxSize},
@@ -940,7 +942,7 @@ func TestNewAndLegacyConsentBothProvided(t *testing.T) {
 		endpoint, _ := NewAmpEndpoint(
 			fakeUUIDGenerator{},
 			mockExchange,
-			newParamsValidator(t),
+			ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, newParamsValidator(t)),
 			&mockAmpStoredReqFetcher{stored},
 			empty_fetcher.EmptyFetcher{},
 			&config.Configuration{
@@ -998,7 +1000,7 @@ func TestAMPSiteExt(t *testing.T) {
 	endpoint, _ := NewAmpEndpoint(
 		fakeUUIDGenerator{},
 		exchange,
-		newParamsValidator(t),
+		ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, newParamsValidator(t)),
 		&mockAmpStoredReqFetcher{stored},
 		empty_fetcher.EmptyFetcher{},
 		&config.Configuration{MaxRequestSize: maxSize},
@@ -1060,7 +1062,7 @@ func TestAmpBadRequests(t *testing.T) {
 	endpoint, _ := NewAmpEndpoint(
 		fakeUUIDGenerator{},
 		&mockAmpExchange{},
-		newParamsValidator(t),
+		ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, newParamsValidator(t)),
 		&mockAmpStoredReqFetcher{data: mockAmpStoredReq},
 		empty_fetcher.EmptyFetcher{},
 		&config.Configuration{MaxRequestSize: maxSize},
@@ -1149,7 +1151,7 @@ func TestAmpDebug(t *testing.T) {
 	endpoint, _ := NewAmpEndpoint(
 		fakeUUIDGenerator{},
 		&mockAmpExchange{},
-		newParamsValidator(t),
+		ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, newParamsValidator(t)),
 		&mockAmpStoredReqFetcher{requests},
 		empty_fetcher.EmptyFetcher{},
 		&config.Configuration{MaxRequestSize: maxSize},
@@ -1285,7 +1287,7 @@ func TestQueryParamOverrides(t *testing.T) {
 	endpoint, _ := NewAmpEndpoint(
 		fakeUUIDGenerator{},
 		&mockAmpExchange{},
-		newParamsValidator(t),
+		ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, newParamsValidator(t)),
 		&mockAmpStoredReqFetcher{requests},
 		empty_fetcher.EmptyFetcher{},
 		&config.Configuration{MaxRequestSize: maxSize},
@@ -1443,7 +1445,7 @@ func (s formatOverrideSpec) execute(t *testing.T) {
 	endpoint, _ := NewAmpEndpoint(
 		fakeUUIDGenerator{},
 		&mockAmpExchange{},
-		newParamsValidator(t),
+		ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, newParamsValidator(t)),
 		&mockAmpStoredReqFetcher{requests},
 		empty_fetcher.EmptyFetcher{},
 		&config.Configuration{MaxRequestSize: maxSize},
@@ -1984,7 +1986,7 @@ func ampObjectTestSetup(t *testing.T, inTagId string, inStoredRequest json.RawMe
 	endpoint, _ := NewAmpEndpoint(
 		fakeUUIDGenerator{id: "foo", err: nil},
 		exchange,
-		newParamsValidator(t),
+		ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, newParamsValidator(t)),
 		mockAmpFetcher,
 		empty_fetcher.EmptyFetcher{},
 		&config.Configuration{MaxRequestSize: maxSize, GenerateRequestID: generateRequestID},
@@ -2037,7 +2039,7 @@ func TestAmpAuctionResponseHeaders(t *testing.T) {
 	endpoint, _ := NewAmpEndpoint(
 		fakeUUIDGenerator{},
 		exchange,
-		newParamsValidator(t),
+		ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, newParamsValidator(t)),
 		&mockAmpStoredReqFetcher{storedRequests},
 		empty_fetcher.EmptyFetcher{},
 		&config.Configuration{MaxRequestSize: maxSize},
@@ -2073,7 +2075,7 @@ func TestRequestWithTargeting(t *testing.T) {
 	endpoint, _ := NewAmpEndpoint(
 		fakeUUIDGenerator{},
 		exchange,
-		newParamsValidator(t),
+		ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, newParamsValidator(t)),
 		&mockAmpStoredReqFetcher{stored},
 		empty_fetcher.EmptyFetcher{},
 		&config.Configuration{MaxRequestSize: maxSize},
@@ -2522,7 +2524,7 @@ func TestAmpAuctionDebugWarningsOnly(t *testing.T) {
 	endpoint, _ := NewAmpEndpoint(
 		fakeUUIDGenerator{},
 		exchange,
-		newParamsValidator(t),
+		ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, newParamsValidator(t)),
 		&mockAmpStoredReqFetcher{storedRequests},
 		empty_fetcher.EmptyFetcher{},
 		&config.Configuration{
diff --git a/endpoints/openrtb2/auction_benchmark_test.go b/endpoints/openrtb2/auction_benchmark_test.go
index 835d2920..d3dbc518 100644
--- a/endpoints/openrtb2/auction_benchmark_test.go
+++ b/endpoints/openrtb2/auction_benchmark_test.go
@@ -19,6 +19,7 @@ import (
 	"github.com/prebid/prebid-server/v2/macros"
 	metricsConfig "github.com/prebid/prebid-server/v2/metrics/config"
 	"github.com/prebid/prebid-server/v2/openrtb_ext"
+	"github.com/prebid/prebid-server/v2/ortb"
 	"github.com/prebid/prebid-server/v2/stored_requests/backends/empty_fetcher"
 	"github.com/prebid/prebid-server/v2/usersync"
 )
@@ -71,6 +72,7 @@ func BenchmarkOpenrtbEndpoint(b *testing.B) {
 	if err != nil {
 		return
 	}
+	requestValidator := ortb.NewRequestValidator(nil, map[string]string{}, paramValidator)
 
 	nilMetrics := &metricsConfig.NilMetricsEngine{}
 
@@ -87,6 +89,7 @@ func BenchmarkOpenrtbEndpoint(b *testing.B) {
 		adapters,
 		nil,
 		&config.Configuration{},
+		requestValidator,
 		map[string]usersync.Syncer{},
 		nilMetrics,
 		infos,
@@ -101,7 +104,7 @@ func BenchmarkOpenrtbEndpoint(b *testing.B) {
 	endpoint, _ := NewEndpoint(
 		fakeUUIDGenerator{},
 		exchange,
-		paramValidator,
+		requestValidator,
 		empty_fetcher.EmptyFetcher{},
 		empty_fetcher.EmptyFetcher{},
 		&config.Configuration{MaxRequestSize: maxSize},
diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go
index fbeddf46..717609e2 100644
--- a/endpoints/openrtb2/auction_test.go
+++ b/endpoints/openrtb2/auction_test.go
@@ -21,8 +21,6 @@ import (
 	"github.com/buger/jsonparser"
 	jsoniter "github.com/json-iterator/go"
 	"github.com/julienschmidt/httprouter"
-	"github.com/prebid/openrtb/v20/native1"
-	nativeRequests "github.com/prebid/openrtb/v20/native1/request"
 	"github.com/prebid/openrtb/v20/openrtb2"
 	"github.com/prebid/prebid-server/v2/analytics"
 	analyticsBuild "github.com/prebid/prebid-server/v2/analytics/build"
@@ -35,6 +33,7 @@ import (
 	"github.com/prebid/prebid-server/v2/metrics"
 	metricsConfig "github.com/prebid/prebid-server/v2/metrics/config"
 	"github.com/prebid/prebid-server/v2/openrtb_ext"
+	"github.com/prebid/prebid-server/v2/ortb"
 	"github.com/prebid/prebid-server/v2/stored_requests/backends/empty_fetcher"
 	"github.com/prebid/prebid-server/v2/stored_responses"
 	"github.com/prebid/prebid-server/v2/util/iputil"
@@ -454,7 +453,7 @@ func TestExplicitUserId(t *testing.T) {
 	endpoint, _ := NewEndpoint(
 		fakeUUIDGenerator{},
 		ex,
-		mockBidderParamValidator{},
+		ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}),
 		empty_fetcher.EmptyFetcher{},
 		empty_fetcher.EmptyFetcher{},
 		cfg,
@@ -512,7 +511,7 @@ func doBadAliasRequest(t *testing.T, filename string, expectMsg string) {
 	endpoint, _ := NewEndpoint(
 		fakeUUIDGenerator{},
 		&nobidExchange{},
-		mockBidderParamValidator{},
+		ortb.NewRequestValidator(bidderMap, disabledBidders, mockBidderParamValidator{}),
 		&mockStoredReqFetcher{},
 		empty_fetcher.EmptyFetcher{},
 		&config.Configuration{MaxRequestSize: maxSize},
@@ -567,32 +566,7 @@ func TestNilExchange(t *testing.T) {
 	_, err := NewEndpoint(
 		fakeUUIDGenerator{},
 		nil,
-		mockBidderParamValidator{},
-		empty_fetcher.EmptyFetcher{},
-		empty_fetcher.EmptyFetcher{},
-		&config.Configuration{MaxRequestSize: maxSize},
-		&metricsConfig.NilMetricsEngine{},
-		analyticsBuild.New(&config.Analytics{}), map[string]string{},
-		[]byte{},
-		openrtb_ext.BuildBidderMap(),
-		empty_fetcher.EmptyFetcher{},
-		hooks.EmptyPlanBuilder{},
-		nil,
-	)
-
-	if err == nil {
-		t.Errorf("NewEndpoint should return an error when given a nil Exchange.")
-	}
-}
-
-// TestNilValidator makes sure we fail when given nil for the BidderParamValidator.
-func TestNilValidator(t *testing.T) {
-	// NewMetrics() will create a new go_metrics MetricsEngine, bypassing the need for a crafted configuration set to support it.
-	// As a side effect this gives us some coverage of the go_metrics piece of the metrics engine.
-	_, err := NewEndpoint(
-		fakeUUIDGenerator{},
-		&nobidExchange{},
-		nil,
+		ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}),
 		empty_fetcher.EmptyFetcher{},
 		empty_fetcher.EmptyFetcher{},
 		&config.Configuration{MaxRequestSize: maxSize},
@@ -607,7 +581,7 @@ func TestNilValidator(t *testing.T) {
 	)
 
 	if err == nil {
-		t.Errorf("NewEndpoint should return an error when given a nil BidderParamValidator.")
+		t.Errorf("NewEndpoint should return an error when given a nil Exchange.")
 	}
 }
 
@@ -618,7 +592,7 @@ func TestExchangeError(t *testing.T) {
 	endpoint, _ := NewEndpoint(
 		fakeUUIDGenerator{},
 		&brokenExchange{},
-		mockBidderParamValidator{},
+		ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}),
 		empty_fetcher.EmptyFetcher{},
 		empty_fetcher.EmptyFetcher{},
 		&config.Configuration{MaxRequestSize: maxSize},
@@ -745,7 +719,7 @@ func TestImplicitIPsEndToEnd(t *testing.T) {
 		endpoint, _ := NewEndpoint(
 			fakeUUIDGenerator{},
 			exchange,
-			mockBidderParamValidator{},
+			ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}),
 			&mockStoredReqFetcher{},
 			empty_fetcher.EmptyFetcher{},
 			cfg,
@@ -945,7 +919,7 @@ func TestImplicitDNTEndToEnd(t *testing.T) {
 		endpoint, _ := NewEndpoint(
 			fakeUUIDGenerator{},
 			exchange,
-			mockBidderParamValidator{},
+			ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}),
 			&mockStoredReqFetcher{},
 			empty_fetcher.EmptyFetcher{},
 			&config.Configuration{MaxRequestSize: maxSize},
@@ -1181,7 +1155,7 @@ func TestStoredRequests(t *testing.T) {
 	deps := &endpointDeps{
 		fakeUUIDGenerator{},
 		&nobidExchange{},
-		mockBidderParamValidator{},
+		ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}),
 		&mockStoredReqFetcher{},
 		empty_fetcher.EmptyFetcher{},
 		empty_fetcher.EmptyFetcher{},
@@ -1631,7 +1605,7 @@ func TestValidateRequest(t *testing.T) {
 	deps := &endpointDeps{
 		fakeUUIDGenerator{},
 		&nobidExchange{},
-		mockBidderParamValidator{},
+		ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}),
 		&mockStoredReqFetcher{},
 		empty_fetcher.EmptyFetcher{},
 		empty_fetcher.EmptyFetcher{},
@@ -2342,7 +2316,7 @@ func TestSetIntegrationType(t *testing.T) {
 	deps := &endpointDeps{
 		fakeUUIDGenerator{},
 		&nobidExchange{},
-		mockBidderParamValidator{},
+		ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}),
 		&mockStoredReqFetcher{},
 		empty_fetcher.EmptyFetcher{},
 		empty_fetcher.EmptyFetcher{},
@@ -2409,7 +2383,7 @@ func TestStoredRequestGenerateUuid(t *testing.T) {
 	deps := &endpointDeps{
 		fakeUUIDGenerator{id: "foo", err: nil},
 		&nobidExchange{},
-		mockBidderParamValidator{},
+		ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}),
 		&mockStoredReqFetcher{},
 		empty_fetcher.EmptyFetcher{},
 		empty_fetcher.EmptyFetcher{},
@@ -2514,7 +2488,7 @@ func TestOversizedRequest(t *testing.T) {
 	deps := &endpointDeps{
 		fakeUUIDGenerator{},
 		&nobidExchange{},
-		mockBidderParamValidator{},
+		ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}),
 		&mockStoredReqFetcher{},
 		empty_fetcher.EmptyFetcher{},
 		empty_fetcher.EmptyFetcher{},
@@ -2554,7 +2528,7 @@ func TestRequestSizeEdgeCase(t *testing.T) {
 	deps := &endpointDeps{
 		fakeUUIDGenerator{},
 		&nobidExchange{},
-		mockBidderParamValidator{},
+		ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}),
 		&mockStoredReqFetcher{},
 		empty_fetcher.EmptyFetcher{},
 		empty_fetcher.EmptyFetcher{},
@@ -2593,7 +2567,7 @@ func TestNoEncoding(t *testing.T) {
 	endpoint, _ := NewEndpoint(
 		fakeUUIDGenerator{},
 		&mockExchange{},
-		mockBidderParamValidator{},
+		ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}),
 		&mockStoredReqFetcher{},
 		empty_fetcher.EmptyFetcher{},
 		&config.Configuration{MaxRequestSize: maxSize},
@@ -2678,7 +2652,7 @@ func TestContentType(t *testing.T) {
 	endpoint, _ := NewEndpoint(
 		fakeUUIDGenerator{},
 		&mockExchange{},
-		mockBidderParamValidator{},
+		ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}),
 		&mockStoredReqFetcher{},
 		empty_fetcher.EmptyFetcher{},
 		&config.Configuration{MaxRequestSize: maxSize},
@@ -2700,241 +2674,6 @@ func TestContentType(t *testing.T) {
 	}
 }
 
-func TestValidateImpExt(t *testing.T) {
-	type testCase struct {
-		description    string
-		impExt         json.RawMessage
-		expectedImpExt string
-		expectedErrs   []error
-	}
-	testGroups := []struct {
-		description string
-		testCases   []testCase
-	}{
-		{
-			"Empty",
-			[]testCase{
-				{
-					description:    "Empty",
-					impExt:         nil,
-					expectedImpExt: "",
-					expectedErrs:   []error{errors.New("request.imp[0].ext is required")},
-				},
-			},
-		},
-		{
-			"Unknown bidder tests",
-			[]testCase{
-				{
-					description:    "Unknown Bidder only",
-					impExt:         json.RawMessage(`{"unknownbidder":{"placement_id":555}}`),
-					expectedImpExt: `{"unknownbidder":{"placement_id":555}}`,
-					expectedErrs:   []error{errors.New("request.imp[0].ext.prebid.bidder contains unknown bidder: unknownbidder. Did you forget an alias in request.ext.prebid.aliases?")},
-				},
-				{
-					description:    "Unknown Prebid Ext Bidder only",
-					impExt:         json.RawMessage(`{"prebid":{"bidder":{"unknownbidder":{"placement_id":555}}}}`),
-					expectedImpExt: `{"prebid":{"bidder":{"unknownbidder":{"placement_id":555}}}}`,
-					expectedErrs:   []error{errors.New("request.imp[0].ext.prebid.bidder contains unknown bidder: unknownbidder. Did you forget an alias in request.ext.prebid.aliases?")},
-				},
-				{
-					description:    "Unknown Prebid Ext Bidder + First Party Data Context",
-					impExt:         json.RawMessage(`{"prebid":{"bidder":{"unknownbidder":{"placement_id":555}}},"context":{"data":{"keywords":"prebid server example"}}}`),
-					expectedImpExt: `{"prebid":{"bidder":{"unknownbidder":{"placement_id":555}}},"context":{"data":{"keywords":"prebid server example"}}}`,
-					expectedErrs:   []error{errors.New("request.imp[0].ext.prebid.bidder contains unknown bidder: unknownbidder. Did you forget an alias in request.ext.prebid.aliases?")},
-				},
-				{
-					description:    "Unknown Bidder + First Party Data Context",
-					impExt:         json.RawMessage(`{"unknownbidder":{"placement_id":555} ,"context":{"data":{"keywords":"prebid server example"}}}`),
-					expectedImpExt: `{"unknownbidder":{"placement_id":555},"context":{"data":{"keywords":"prebid server example"}}}`,
-					expectedErrs:   []error{errors.New("request.imp[0].ext.prebid.bidder contains unknown bidder: unknownbidder. Did you forget an alias in request.ext.prebid.aliases?")},
-				},
-				{
-					description:    "Unknown Bidder + Disabled Bidder",
-					impExt:         json.RawMessage(`{"unknownbidder":{"placement_id":555},"disabledbidder":{"foo":"bar"}}`),
-					expectedImpExt: `{"unknownbidder":{"placement_id":555},"disabledbidder":{"foo":"bar"}}`,
-					expectedErrs:   []error{errors.New("request.imp[0].ext.prebid.bidder contains unknown bidder: unknownbidder. Did you forget an alias in request.ext.prebid.aliases?")},
-				},
-				{
-					description:    "Unknown Bidder + Disabled Prebid Ext Bidder",
-					impExt:         json.RawMessage(`{"unknownbidder":{"placement_id":555},"prebid":{"bidder":{"disabledbidder":{"foo":"bar"}}}}`),
-					expectedImpExt: `{"unknownbidder":{"placement_id":555},"prebid":{"bidder":{"disabledbidder":{"foo":"bar"}}}}`,
-					expectedErrs:   []error{errors.New("request.imp[0].ext.prebid.bidder contains unknown bidder: unknownbidder. Did you forget an alias in request.ext.prebid.aliases?")},
-				},
-			},
-		},
-		{
-			"Disabled bidder tests",
-			[]testCase{
-				{
-					description:    "Disabled Bidder",
-					impExt:         json.RawMessage(`{"disabledbidder":{"foo":"bar"}}`),
-					expectedImpExt: `{"disabledbidder":{"foo":"bar"}}`,
-					expectedErrs: []error{
-						&errortypes.BidderTemporarilyDisabled{Message: "The bidder 'disabledbidder' has been disabled."},
-						errors.New("request.imp[0].ext.prebid.bidder must contain at least one bidder"),
-					},
-					// if only bidder(s) found in request.imp[x].ext.{biddername} or request.imp[x].ext.prebid.bidder.{biddername} are disabled, return error
-				},
-				{
-					description:    "Disabled Prebid Ext Bidder",
-					impExt:         json.RawMessage(`{"prebid":{"bidder":{"disabledbidder":{"foo":"bar"}}}}`),
-					expectedImpExt: `{"prebid":{"bidder":{"disabledbidder":{"foo":"bar"}}}}`,
-					expectedErrs: []error{
-						&errortypes.BidderTemporarilyDisabled{Message: "The bidder 'disabledbidder' has been disabled."},
-						errors.New("request.imp[0].ext.prebid.bidder must contain at least one bidder"),
-					},
-				},
-				{
-					description:    "Disabled Bidder + First Party Data Context",
-					impExt:         json.RawMessage(`{"disabledbidder":{"foo":"bar"},"context":{"data":{"keywords":"prebid server example"}}}`),
-					expectedImpExt: `{"disabledbidder":{"foo":"bar"},"context":{"data":{"keywords":"prebid server example"}}}`,
-					expectedErrs: []error{
-						&errortypes.BidderTemporarilyDisabled{Message: "The bidder 'disabledbidder' has been disabled."},
-						errors.New("request.imp[0].ext.prebid.bidder must contain at least one bidder"),
-					},
-				},
-				{
-					description:    "Disabled Prebid Ext Bidder + First Party Data Context",
-					impExt:         json.RawMessage(`{"prebid":{"bidder":{"disabledbidder":{"foo":"bar"}}},"context":{"data":{"keywords":"prebid server example"}}}`),
-					expectedImpExt: `{"prebid":{"bidder":{"disabledbidder":{"foo":"bar"}}},"context":{"data":{"keywords":"prebid server example"}}}`,
-					expectedErrs: []error{
-						&errortypes.BidderTemporarilyDisabled{Message: "The bidder 'disabledbidder' has been disabled."},
-						errors.New("request.imp[0].ext.prebid.bidder must contain at least one bidder"),
-					},
-				},
-			},
-		},
-		{
-			"First Party only",
-			[]testCase{
-				{
-					description:    "First Party Data Context",
-					impExt:         json.RawMessage(`{"context":{"data":{"keywords":"prebid server example"}}}`),
-					expectedImpExt: `{"context":{"data":{"keywords":"prebid server example"}}}`,
-					expectedErrs: []error{
-						errors.New("request.imp[0].ext.prebid.bidder must contain at least one bidder"),
-					},
-				},
-			},
-		},
-		{
-			"Valid bidder tests",
-			[]testCase{
-				{
-					description:    "Valid bidder root ext",
-					impExt:         json.RawMessage(`{"appnexus":{"placement_id":555}}`),
-					expectedImpExt: `{"prebid":{"bidder":{"appnexus":{"placement_id":555}}}}`,
-					expectedErrs:   []error{},
-				},
-				{
-					description:    "Valid bidder in prebid field",
-					impExt:         json.RawMessage(`{"prebid":{"bidder":{"appnexus":{"placement_id":555}}}}`),
-					expectedImpExt: `{"prebid":{"bidder":{"appnexus":{"placement_id":555}}}}`,
-					expectedErrs:   []error{},
-				},
-				{
-					description:    "Valid Bidder + First Party Data Context",
-					impExt:         json.RawMessage(`{"appnexus":{"placement_id":555},"context":{"data":{"keywords":"prebid server example"}}}`),
-					expectedImpExt: `{"prebid":{"bidder":{"appnexus":{"placement_id":555}}},"context":{"data":{"keywords":"prebid server example"}}}`,
-					expectedErrs:   []error{},
-				},
-				{
-					description:    "Valid Prebid Ext Bidder + First Party Data Context",
-					impExt:         json.RawMessage(`{"prebid":{"bidder":{"appnexus":{"placement_id":555}}} ,"context":{"data":{"keywords":"prebid server example"}}}`),
-					expectedImpExt: `{"prebid":{"bidder":{"appnexus":{"placement_id":555}}},"context":{"data":{"keywords":"prebid server example"}}}`,
-					expectedErrs:   []error{},
-				},
-				{
-					description:    "Valid Bidder + Unknown Bidder",
-					impExt:         json.RawMessage(`{"appnexus":{"placement_id":555},"unknownbidder":{"placement_id":555}}`),
-					expectedImpExt: `{"appnexus":{"placement_id":555},"unknownbidder":{"placement_id":555}}`,
-					expectedErrs:   []error{errors.New("request.imp[0].ext.prebid.bidder contains unknown bidder: unknownbidder. Did you forget an alias in request.ext.prebid.aliases?")},
-				},
-				{
-					description:    "Valid Bidder + Disabled Bidder",
-					impExt:         json.RawMessage(`{"appnexus":{"placement_id":555},"disabledbidder":{"foo":"bar"}}`),
-					expectedImpExt: `{"prebid":{"bidder":{"appnexus":{"placement_id":555}}}}`,
-					expectedErrs:   []error{&errortypes.BidderTemporarilyDisabled{Message: "The bidder 'disabledbidder' has been disabled."}},
-				},
-				{
-					description:    "Valid Bidder + Disabled Bidder + First Party Data Context",
-					impExt:         json.RawMessage(`{"appnexus":{"placement_id":555},"disabledbidder":{"foo":"bar"},"context":{"data":{"keywords":"prebid server example"}}}`),
-					expectedImpExt: `{"prebid":{"bidder":{"appnexus":{"placement_id":555}}},"context":{"data":{"keywords":"prebid server example"}}}`,
-					expectedErrs:   []error{&errortypes.BidderTemporarilyDisabled{Message: "The bidder 'disabledbidder' has been disabled."}},
-				},
-				{
-					description:    "Valid Bidder + Disabled Bidder + Unknown Bidder + First Party Data Context",
-					impExt:         json.RawMessage(`{"appnexus":{"placement_id":555},"disabledbidder":{"foo":"bar"},"unknownbidder":{"placement_id":555},"context":{"data":{"keywords":"prebid server example"}}}`),
-					expectedImpExt: `{"appnexus":{"placement_id":555},"disabledbidder":{"foo":"bar"},"unknownbidder":{"placement_id":555},"context":{"data":{"keywords":"prebid server example"}}}`,
-					expectedErrs:   []error{errors.New("request.imp[0].ext.prebid.bidder contains unknown bidder: unknownbidder. Did you forget an alias in request.ext.prebid.aliases?")},
-				},
-				{
-					description:    "Valid Prebid Ext Bidder + Disabled Bidder Ext",
-					impExt:         json.RawMessage(`{"prebid":{"bidder":{"appnexus":{"placement_id":555},"disabledbidder":{"foo":"bar"}}}}`),
-					expectedImpExt: `{"prebid":{"bidder":{"appnexus":{"placement_id": 555}}}}`,
-					expectedErrs:   []error{&errortypes.BidderTemporarilyDisabled{Message: "The bidder 'disabledbidder' has been disabled."}},
-				},
-				{
-					description:    "Valid Prebid Ext Bidder + Disabled Ext Bidder + First Party Data Context",
-					impExt:         json.RawMessage(`{"prebid":{"bidder":{"appnexus":{"placement_id":555},"disabledbidder":{"foo":"bar"}}},"context":{"data":{"keywords":"prebid server example"}}}`),
-					expectedImpExt: `{"prebid":{"bidder":{"appnexus":{"placement_id": 555}}},"context":{"data":{"keywords":"prebid server example"}}}`,
-					expectedErrs:   []error{&errortypes.BidderTemporarilyDisabled{Message: "The bidder 'disabledbidder' has been disabled."}},
-				},
-				{
-					description:    "Valid Prebid Ext Bidder + Disabled Ext Bidder + Unknown Ext + First Party Data Context",
-					impExt:         json.RawMessage(`{"prebid":{"bidder":{"appnexus":{"placement_id":555},"disabledbidder":{"foo":"bar"},"unknownbidder":{"placement_id":555}}},"context":{"data":{"keywords":"prebid server example"}}}`),
-					expectedImpExt: `{"prebid":{"bidder":{"appnexus":{"placement_id":555},"disabledbidder":{"foo":"bar"},"unknownbidder":{"placement_id":555}}},"context":{"data":{"keywords":"prebid server example"}}}`,
-					expectedErrs:   []error{errors.New("request.imp[0].ext.prebid.bidder contains unknown bidder: unknownbidder. Did you forget an alias in request.ext.prebid.aliases?")},
-				},
-			},
-		},
-	}
-
-	deps := &endpointDeps{
-		fakeUUIDGenerator{},
-		&nobidExchange{},
-		mockBidderParamValidator{},
-		&mockStoredReqFetcher{},
-		empty_fetcher.EmptyFetcher{},
-		empty_fetcher.EmptyFetcher{},
-		&config.Configuration{MaxRequestSize: int64(8096)},
-		&metricsConfig.NilMetricsEngine{},
-		analyticsBuild.New(&config.Analytics{}),
-		map[string]string{"disabledbidder": "The bidder 'disabledbidder' has been disabled."},
-		false,
-		[]byte{},
-		openrtb_ext.BuildBidderMap(),
-		nil,
-		nil,
-		hardcodedResponseIPValidator{response: true},
-		empty_fetcher.EmptyFetcher{},
-		hooks.EmptyPlanBuilder{},
-		nil,
-		openrtb_ext.NormalizeBidderName,
-	}
-
-	for _, group := range testGroups {
-		for _, test := range group.testCases {
-			t.Run(test.description, func(t *testing.T) {
-				imp := &openrtb2.Imp{Ext: test.impExt}
-				impWrapper := &openrtb_ext.ImpWrapper{Imp: imp}
-
-				errs := deps.validateImpExt(impWrapper, nil, 0, false, nil)
-
-				assert.NoError(t, impWrapper.RebuildImp(), test.description+":rebuild_imp")
-
-				if len(test.expectedImpExt) > 0 {
-					assert.JSONEq(t, test.expectedImpExt, string(imp.Ext), "imp.ext JSON does not match expected. Test: %s. %s\n", group.description, test.description)
-				} else {
-					assert.Empty(t, imp.Ext, "imp.ext expected to be empty but was: %s. Test: %s. %s\n", string(imp.Ext), group.description, test.description)
-				}
-				assert.Equal(t, test.expectedErrs, errs, "errs slice does not match expected. Test: %s. %s\n", group.description, test.description)
-			})
-		}
-	}
-}
-
 func validRequest(t *testing.T, filename string) string {
 	requestData, err := os.ReadFile("sample-requests/valid-whole/supplementary/" + filename)
 	if err != nil {
@@ -2950,7 +2689,7 @@ func TestCurrencyTrunc(t *testing.T) {
 	deps := &endpointDeps{
 		fakeUUIDGenerator{},
 		&nobidExchange{},
-		mockBidderParamValidator{},
+		ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}),
 		&mockStoredReqFetcher{},
 		empty_fetcher.EmptyFetcher{},
 		empty_fetcher.EmptyFetcher{},
@@ -2999,7 +2738,7 @@ func TestCCPAInvalid(t *testing.T) {
 	deps := &endpointDeps{
 		fakeUUIDGenerator{},
 		&nobidExchange{},
-		mockBidderParamValidator{},
+		ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}),
 		&mockStoredReqFetcher{},
 		empty_fetcher.EmptyFetcher{},
 		empty_fetcher.EmptyFetcher{},
@@ -3052,7 +2791,7 @@ func TestNoSaleInvalid(t *testing.T) {
 	deps := &endpointDeps{
 		fakeUUIDGenerator{},
 		&nobidExchange{},
-		mockBidderParamValidator{},
+		ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}),
 		&mockStoredReqFetcher{},
 		empty_fetcher.EmptyFetcher{},
 		empty_fetcher.EmptyFetcher{},
@@ -3108,7 +2847,7 @@ func TestValidateSourceTID(t *testing.T) {
 	deps := &endpointDeps{
 		fakeUUIDGenerator{},
 		&nobidExchange{},
-		mockBidderParamValidator{},
+		ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}),
 		&mockStoredReqFetcher{},
 		empty_fetcher.EmptyFetcher{},
 		empty_fetcher.EmptyFetcher{},
@@ -3154,7 +2893,7 @@ func TestSChainInvalid(t *testing.T) {
 	deps := &endpointDeps{
 		fakeUUIDGenerator{},
 		&nobidExchange{},
-		mockBidderParamValidator{},
+		ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}),
 		&mockStoredReqFetcher{},
 		empty_fetcher.EmptyFetcher{},
 		empty_fetcher.EmptyFetcher{},
@@ -3723,7 +3462,7 @@ func TestEidPermissionsInvalid(t *testing.T) {
 	deps := &endpointDeps{
 		fakeUUIDGenerator{},
 		&nobidExchange{},
-		mockBidderParamValidator{},
+		ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}),
 		&mockStoredReqFetcher{},
 		empty_fetcher.EmptyFetcher{},
 		empty_fetcher.EmptyFetcher{},
@@ -4003,7 +3742,7 @@ func TestIOS14EndToEnd(t *testing.T) {
 	endpoint, _ := NewEndpoint(
 		fakeUUIDGenerator{},
 		exchange,
-		mockBidderParamValidator{},
+		ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}),
 		&mockStoredReqFetcher{},
 		empty_fetcher.EmptyFetcher{},
 		&config.Configuration{MaxRequestSize: maxSize},
@@ -4065,7 +3804,7 @@ func TestAuctionWarnings(t *testing.T) {
 	deps := &endpointDeps{
 		fakeUUIDGenerator{},
 		&warningsCheckExchange{},
-		mockBidderParamValidator{},
+		ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}),
 		&mockStoredReqFetcher{},
 		empty_fetcher.EmptyFetcher{},
 		empty_fetcher.EmptyFetcher{},
@@ -4112,7 +3851,7 @@ func TestParseRequestParseImpInfoError(t *testing.T) {
 	deps := &endpointDeps{
 		fakeUUIDGenerator{},
 		&warningsCheckExchange{},
-		mockBidderParamValidator{},
+		ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}),
 		&mockStoredReqFetcher{},
 		empty_fetcher.EmptyFetcher{},
 		empty_fetcher.EmptyFetcher{},
@@ -4193,7 +3932,7 @@ func TestParseGzipedRequest(t *testing.T) {
 	deps := &endpointDeps{
 		fakeUUIDGenerator{},
 		&warningsCheckExchange{},
-		mockBidderParamValidator{},
+		ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}),
 		&mockStoredReqFetcher{},
 		empty_fetcher.EmptyFetcher{},
 		empty_fetcher.EmptyFetcher{},
@@ -4245,425 +3984,6 @@ func TestParseGzipedRequest(t *testing.T) {
 	}
 }
 
-func TestValidateNativeContextTypes(t *testing.T) {
-	impIndex := 4
-
-	testCases := []struct {
-		description      string
-		givenContextType native1.ContextType
-		givenSubType     native1.ContextSubType
-		expectedError    string
-	}{
-		{
-			description:      "No Types Specified",
-			givenContextType: 0,
-			givenSubType:     0,
-			expectedError:    "",
-		},
-		{
-			description:      "All Types Exchange Specific",
-			givenContextType: 500,
-			givenSubType:     500,
-			expectedError:    "",
-		},
-		{
-			description:      "Context Type Known Value - Sub Type Unspecified",
-			givenContextType: 1,
-			givenSubType:     0,
-			expectedError:    "",
-		},
-		{
-			description:      "Context Type Negative",
-			givenContextType: -1,
-			givenSubType:     0,
-			expectedError:    "request.imp[4].native.request.context is invalid. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39",
-		},
-		{
-			description:      "Context Type Just Above Range",
-			givenContextType: 4, // Range is currently 1-3
-			givenSubType:     0,
-			expectedError:    "request.imp[4].native.request.context is invalid. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39",
-		},
-		{
-			description:      "Sub Type Negative",
-			givenContextType: 1,
-			givenSubType:     -1,
-			expectedError:    "request.imp[4].native.request.contextsubtype value can't be less than 0. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39",
-		},
-		{
-			description:      "Content - Sub Type Just Below Range",
-			givenContextType: 1, // Content constant
-			givenSubType:     9, // Content range is currently 10-15
-			expectedError:    "request.imp[4].native.request.contextsubtype is invalid. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39",
-		},
-		{
-			description:      "Content - Sub Type In Range",
-			givenContextType: 1,  // Content constant
-			givenSubType:     10, // Content range is currently 10-15
-			expectedError:    "",
-		},
-		{
-			description:      "Content - Sub Type In Range - Context Type Exchange Specific Boundary",
-			givenContextType: 500,
-			givenSubType:     10, // Content range is currently 10-15
-			expectedError:    "",
-		},
-		{
-			description:      "Content - Sub Type In Range - Context Type Exchange Specific Boundary + 1",
-			givenContextType: 501,
-			givenSubType:     10, // Content range is currently 10-15
-			expectedError:    "",
-		},
-		{
-			description:      "Content - Sub Type Just Above Range",
-			givenContextType: 1,  // Content constant
-			givenSubType:     16, // Content range is currently 10-15
-			expectedError:    "request.imp[4].native.request.contextsubtype is invalid. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39",
-		},
-		{
-			description:      "Content - Sub Type Exchange Specific Boundary",
-			givenContextType: 1, // Content constant
-			givenSubType:     500,
-			expectedError:    "",
-		},
-		{
-			description:      "Content - Sub Type Exchange Specific Boundary + 1",
-			givenContextType: 1, // Content constant
-			givenSubType:     501,
-			expectedError:    "",
-		},
-		{
-			description:      "Content - Invalid Context Type",
-			givenContextType: 2,  // Not content constant
-			givenSubType:     10, // Content range is currently 10-15
-			expectedError:    "request.imp[4].native.request.context is 2, but contextsubtype is 10. This is an invalid combination. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39",
-		},
-		{
-			description:      "Social - Sub Type Just Below Range",
-			givenContextType: 2,  // Social constant
-			givenSubType:     19, // Social range is currently 20-22
-			expectedError:    "request.imp[4].native.request.contextsubtype is invalid. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39",
-		},
-		{
-			description:      "Social - Sub Type In Range",
-			givenContextType: 2,  // Social constant
-			givenSubType:     20, // Social range is currently 20-22
-			expectedError:    "",
-		},
-		{
-			description:      "Social - Sub Type In Range - Context Type Exchange Specific Boundary",
-			givenContextType: 500,
-			givenSubType:     20, // Social range is currently 20-22
-			expectedError:    "",
-		},
-		{
-			description:      "Social - Sub Type In Range - Context Type Exchange Specific Boundary + 1",
-			givenContextType: 501,
-			givenSubType:     20, // Social range is currently 20-22
-			expectedError:    "",
-		},
-		{
-			description:      "Social - Sub Type Just Above Range",
-			givenContextType: 2,  // Social constant
-			givenSubType:     23, // Social range is currently 20-22
-			expectedError:    "request.imp[4].native.request.contextsubtype is invalid. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39",
-		},
-		{
-			description:      "Social - Sub Type Exchange Specific Boundary",
-			givenContextType: 2, // Social constant
-			givenSubType:     500,
-			expectedError:    "",
-		},
-		{
-			description:      "Social - Sub Type Exchange Specific Boundary + 1",
-			givenContextType: 2, // Social constant
-			givenSubType:     501,
-			expectedError:    "",
-		},
-		{
-			description:      "Social - Invalid Context Type",
-			givenContextType: 3,  // Not social constant
-			givenSubType:     20, // Social range is currently 20-22
-			expectedError:    "request.imp[4].native.request.context is 3, but contextsubtype is 20. This is an invalid combination. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39",
-		},
-		{
-			description:      "Product - Sub Type Just Below Range",
-			givenContextType: 3,  // Product constant
-			givenSubType:     29, // Product range is currently 30-32
-			expectedError:    "request.imp[4].native.request.contextsubtype is invalid. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39",
-		},
-		{
-			description:      "Product - Sub Type In Range",
-			givenContextType: 3,  // Product constant
-			givenSubType:     30, // Product range is currently 30-32
-			expectedError:    "",
-		},
-		{
-			description:      "Product - Sub Type In Range - Context Type Exchange Specific Boundary",
-			givenContextType: 500,
-			givenSubType:     30, // Product range is currently 30-32
-			expectedError:    "",
-		},
-		{
-			description:      "Product - Sub Type In Range - Context Type Exchange Specific Boundary + 1",
-			givenContextType: 501,
-			givenSubType:     30, // Product range is currently 30-32
-			expectedError:    "",
-		},
-		{
-			description:      "Product - Sub Type Just Above Range",
-			givenContextType: 3,  // Product constant
-			givenSubType:     33, // Product range is currently 30-32
-			expectedError:    "request.imp[4].native.request.contextsubtype is invalid. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39",
-		},
-		{
-			description:      "Product - Sub Type Exchange Specific Boundary",
-			givenContextType: 3, // Product constant
-			givenSubType:     500,
-			expectedError:    "",
-		},
-		{
-			description:      "Product - Sub Type Exchange Specific Boundary + 1",
-			givenContextType: 3, // Product constant
-			givenSubType:     501,
-			expectedError:    "",
-		},
-		{
-			description:      "Product - Invalid Context Type",
-			givenContextType: 1,  // Not product constant
-			givenSubType:     30, // Product range is currently 30-32
-			expectedError:    "request.imp[4].native.request.context is 1, but contextsubtype is 30. This is an invalid combination. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39",
-		},
-	}
-
-	for _, test := range testCases {
-		err := validateNativeContextTypes(test.givenContextType, test.givenSubType, impIndex)
-		if test.expectedError == "" {
-			assert.NoError(t, err, test.description)
-		} else {
-			assert.EqualError(t, err, test.expectedError, test.description)
-		}
-	}
-}
-
-func TestValidateNativePlacementType(t *testing.T) {
-	impIndex := 4
-
-	testCases := []struct {
-		description        string
-		givenPlacementType native1.PlacementType
-		expectedError      string
-	}{
-		{
-			description:        "Not Specified",
-			givenPlacementType: 0,
-			expectedError:      "",
-		},
-		{
-			description:        "Known Value",
-			givenPlacementType: 1, // Range is currently 1-4
-			expectedError:      "",
-		},
-		{
-			description:        "Exchange Specific - Boundary",
-			givenPlacementType: 500,
-			expectedError:      "",
-		},
-		{
-			description:        "Exchange Specific - Boundary + 1",
-			givenPlacementType: 501,
-			expectedError:      "",
-		},
-		{
-			description:        "Negative",
-			givenPlacementType: -1,
-			expectedError:      "request.imp[4].native.request.plcmttype is invalid. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=40",
-		},
-		{
-			description:        "Just Above Range",
-			givenPlacementType: 5, // Range is currently 1-4
-			expectedError:      "request.imp[4].native.request.plcmttype is invalid. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=40",
-		},
-	}
-
-	for _, test := range testCases {
-		err := validateNativePlacementType(test.givenPlacementType, impIndex)
-		if test.expectedError == "" {
-			assert.NoError(t, err, test.description)
-		} else {
-			assert.EqualError(t, err, test.expectedError, test.description)
-		}
-	}
-}
-
-func TestValidateNativeEventTracker(t *testing.T) {
-	impIndex := 4
-	eventIndex := 8
-
-	testCases := []struct {
-		description   string
-		givenEvent    nativeRequests.EventTracker
-		expectedError string
-	}{
-		{
-			description: "Valid",
-			givenEvent: nativeRequests.EventTracker{
-				Event:   1,
-				Methods: []native1.EventTrackingMethod{1},
-			},
-			expectedError: "",
-		},
-		{
-			description: "Event - Exchange Specific - Boundary",
-			givenEvent: nativeRequests.EventTracker{
-				Event:   500,
-				Methods: []native1.EventTrackingMethod{1},
-			},
-			expectedError: "",
-		},
-		{
-			description: "Event - Exchange Specific - Boundary + 1",
-			givenEvent: nativeRequests.EventTracker{
-				Event:   501,
-				Methods: []native1.EventTrackingMethod{1},
-			},
-			expectedError: "",
-		},
-		{
-			description: "Event - Negative",
-			givenEvent: nativeRequests.EventTracker{
-				Event:   -1,
-				Methods: []native1.EventTrackingMethod{1},
-			},
-			expectedError: "request.imp[4].native.request.eventtrackers[8].event is invalid. See section 7.6: https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=43",
-		},
-		{
-			description: "Event - Just Above Range",
-			givenEvent: nativeRequests.EventTracker{
-				Event:   5, // Range is currently 1-4
-				Methods: []native1.EventTrackingMethod{1},
-			},
-			expectedError: "request.imp[4].native.request.eventtrackers[8].event is invalid. See section 7.6: https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=43",
-		},
-		{
-			description: "Methods - Many Valid",
-			givenEvent: nativeRequests.EventTracker{
-				Event:   1,
-				Methods: []native1.EventTrackingMethod{1, 2},
-			},
-			expectedError: "",
-		},
-		{
-			description: "Methods - Empty",
-			givenEvent: nativeRequests.EventTracker{
-				Event:   1,
-				Methods: []native1.EventTrackingMethod{},
-			},
-			expectedError: "request.imp[4].native.request.eventtrackers[8].method is required. See section 7.7: https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=43",
-		},
-		{
-			description: "Methods - Exchange Specific - Boundary",
-			givenEvent: nativeRequests.EventTracker{
-				Event:   1,
-				Methods: []native1.EventTrackingMethod{500},
-			},
-			expectedError: "",
-		},
-		{
-			description: "Methods - Exchange Specific - Boundary + 1",
-			givenEvent: nativeRequests.EventTracker{
-				Event:   1,
-				Methods: []native1.EventTrackingMethod{501},
-			},
-			expectedError: "",
-		},
-		{
-			description: "Methods - Negative",
-			givenEvent: nativeRequests.EventTracker{
-				Event:   1,
-				Methods: []native1.EventTrackingMethod{-1},
-			},
-			expectedError: "request.imp[4].native.request.eventtrackers[8].methods[0] is invalid. See section 7.7: https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=43",
-		},
-		{
-			description: "Methods - Just Above Range",
-			givenEvent: nativeRequests.EventTracker{
-				Event:   1,
-				Methods: []native1.EventTrackingMethod{3}, // Known values are currently 1-2
-			},
-			expectedError: "request.imp[4].native.request.eventtrackers[8].methods[0] is invalid. See section 7.7: https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=43",
-		},
-		{
-			description: "Methods - Mixed Valid + Invalid",
-			givenEvent: nativeRequests.EventTracker{
-				Event:   1,
-				Methods: []native1.EventTrackingMethod{1, -1},
-			},
-			expectedError: "request.imp[4].native.request.eventtrackers[8].methods[1] is invalid. See section 7.7: https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=43",
-		},
-	}
-
-	for _, test := range testCases {
-		err := validateNativeEventTracker(test.givenEvent, impIndex, eventIndex)
-		if test.expectedError == "" {
-			assert.NoError(t, err, test.description)
-		} else {
-			assert.EqualError(t, err, test.expectedError, test.description)
-		}
-	}
-}
-
-func TestValidateNativeAssetData(t *testing.T) {
-	impIndex := 4
-	assetIndex := 8
-
-	testCases := []struct {
-		description   string
-		givenData     nativeRequests.Data
-		expectedError string
-	}{
-		{
-			description:   "Valid",
-			givenData:     nativeRequests.Data{Type: 1},
-			expectedError: "",
-		},
-		{
-			description:   "Exchange Specific - Boundary",
-			givenData:     nativeRequests.Data{Type: 500},
-			expectedError: "",
-		},
-		{
-			description:   "Exchange Specific - Boundary + 1",
-			givenData:     nativeRequests.Data{Type: 501},
-			expectedError: "",
-		},
-		{
-			description:   "Not Specified",
-			givenData:     nativeRequests.Data{},
-			expectedError: "request.imp[4].native.request.assets[8].data.type is invalid. See section 7.4: https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=40",
-		},
-		{
-			description:   "Negative",
-			givenData:     nativeRequests.Data{Type: -1},
-			expectedError: "request.imp[4].native.request.assets[8].data.type is invalid. See section 7.4: https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=40",
-		},
-		{
-			description:   "Just Above Range",
-			givenData:     nativeRequests.Data{Type: 13}, // Range is currently 1-12
-			expectedError: "request.imp[4].native.request.assets[8].data.type is invalid. See section 7.4: https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=40",
-		},
-	}
-
-	for _, test := range testCases {
-		err := validateNativeAssetData(&test.givenData, impIndex, assetIndex)
-		if test.expectedError == "" {
-			assert.NoError(t, err, test.description)
-		} else {
-			assert.EqualError(t, err, test.expectedError, test.description)
-		}
-	}
-}
-
 func TestAuctionResponseHeaders(t *testing.T) {
 	testCases := []struct {
 		description     string
@@ -4721,7 +4041,7 @@ func TestAuctionResponseHeaders(t *testing.T) {
 	endpoint, _ := NewEndpoint(
 		fakeUUIDGenerator{},
 		exchange,
-		mockBidderParamValidator{},
+		ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}),
 		empty_fetcher.EmptyFetcher{},
 		empty_fetcher.EmptyFetcher{},
 		&config.Configuration{MaxRequestSize: maxSize},
@@ -4752,38 +4072,6 @@ func TestAuctionResponseHeaders(t *testing.T) {
 
 // Test stored request data
 
-func TestValidateBanner(t *testing.T) {
-	impIndex := 0
-
-	testCases := []struct {
-		description    string
-		banner         *openrtb2.Banner
-		impIndex       int
-		isInterstitial bool
-		expectedError  error
-	}{
-		{
-			description:    "isInterstitial Equals False (not set to 1)",
-			banner:         &openrtb2.Banner{W: nil, H: nil, Format: nil},
-			impIndex:       impIndex,
-			isInterstitial: false,
-			expectedError:  errors.New("request.imp[0].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements."),
-		},
-		{
-			description:    "isInterstitial Equals True (is set to 1)",
-			banner:         &openrtb2.Banner{W: nil, H: nil, Format: nil},
-			impIndex:       impIndex,
-			isInterstitial: true,
-			expectedError:  nil,
-		},
-	}
-
-	for _, test := range testCases {
-		result := validateBanner(test.banner, test.impIndex, test.isInterstitial)
-		assert.Equal(t, test.expectedError, result, test.description)
-	}
-}
-
 func TestParseRequestMergeBidderParams(t *testing.T) {
 	tests := []struct {
 		name               string
@@ -4820,7 +4108,7 @@ func TestParseRequestMergeBidderParams(t *testing.T) {
 			deps := &endpointDeps{
 				fakeUUIDGenerator{},
 				&warningsCheckExchange{},
-				mockBidderParamValidator{},
+				ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}),
 				&mockStoredReqFetcher{},
 				empty_fetcher.EmptyFetcher{},
 				empty_fetcher.EmptyFetcher{},
@@ -4924,7 +4212,7 @@ func TestParseRequestStoredResponses(t *testing.T) {
 			deps := &endpointDeps{
 				fakeUUIDGenerator{},
 				&warningsCheckExchange{},
-				mockBidderParamValidator{},
+				ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}),
 				&mockStoredReqFetcher{},
 				empty_fetcher.EmptyFetcher{},
 				empty_fetcher.EmptyFetcher{},
@@ -5037,7 +4325,7 @@ func TestParseRequestStoredBidResponses(t *testing.T) {
 			deps := &endpointDeps{
 				fakeUUIDGenerator{},
 				&warningsCheckExchange{},
-				mockBidderParamValidator{},
+				ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}),
 				&mockStoredReqFetcher{},
 				empty_fetcher.EmptyFetcher{},
 				empty_fetcher.EmptyFetcher{},
@@ -5075,7 +4363,7 @@ func TestValidateStoredResp(t *testing.T) {
 	deps := &endpointDeps{
 		fakeUUIDGenerator{},
 		&nobidExchange{},
-		mockBidderParamValidator{},
+		ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}),
 		&mockStoredReqFetcher{},
 		empty_fetcher.EmptyFetcher{},
 		empty_fetcher.EmptyFetcher{},
@@ -5893,7 +5181,7 @@ func TestParseRequestMultiBid(t *testing.T) {
 			deps := &endpointDeps{
 				fakeUUIDGenerator{},
 				&warningsCheckExchange{},
-				mockBidderParamValidator{},
+				ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}),
 				&mockStoredReqFetcher{},
 				empty_fetcher.EmptyFetcher{},
 				empty_fetcher.EmptyFetcher{},
@@ -6440,7 +5728,7 @@ func TestValidateRequestCookieDeprecation(t *testing.T) {
 	deps := &endpointDeps{
 		fakeUUIDGenerator{},
 		&warningsCheckExchange{},
-		mockBidderParamValidator{},
+		ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}),
 		&mockStoredReqFetcher{},
 		empty_fetcher.EmptyFetcher{},
 		&mockAccountFetcher{},
diff --git a/endpoints/openrtb2/test_utils.go b/endpoints/openrtb2/test_utils.go
index 66eb64fa..b2f28267 100644
--- a/endpoints/openrtb2/test_utils.go
+++ b/endpoints/openrtb2/test_utils.go
@@ -34,6 +34,7 @@ import (
 	"github.com/prebid/prebid-server/v2/metrics"
 	metricsConfig "github.com/prebid/prebid-server/v2/metrics/config"
 	"github.com/prebid/prebid-server/v2/openrtb_ext"
+	"github.com/prebid/prebid-server/v2/ortb"
 	pbc "github.com/prebid/prebid-server/v2/prebid_cache_client"
 	"github.com/prebid/prebid-server/v2/stored_requests"
 	"github.com/prebid/prebid-server/v2/stored_requests/backends/empty_fetcher"
@@ -1196,7 +1197,7 @@ func (te *exchangeTestWrapper) HoldAuction(ctx context.Context, r *exchange.Auct
 }
 
 // buildTestExchange returns an exchange with mock bidder servers and mock currency conversion server
-func buildTestExchange(testCfg *testConfigValues, adapterMap map[openrtb_ext.BidderName]exchange.AdaptedBidder, mockBidServersArray []*httptest.Server, mockCurrencyRatesServer *httptest.Server, bidderInfos config.BidderInfos, cfg *config.Configuration, met metrics.MetricsEngine, mockFetcher stored_requests.CategoryFetcher) (exchange.Exchange, []*httptest.Server) {
+func buildTestExchange(testCfg *testConfigValues, adapterMap map[openrtb_ext.BidderName]exchange.AdaptedBidder, mockBidServersArray []*httptest.Server, mockCurrencyRatesServer *httptest.Server, bidderInfos config.BidderInfos, cfg *config.Configuration, met metrics.MetricsEngine, mockFetcher stored_requests.CategoryFetcher, requestValidator ortb.RequestValidator) (exchange.Exchange, []*httptest.Server) {
 	if len(testCfg.MockBidders) == 0 {
 		testCfg.MockBidders = append(testCfg.MockBidders, mockBidderHandler{BidderName: "appnexus", Currency: "USD", Price: 0.00})
 	}
@@ -1219,6 +1220,7 @@ func buildTestExchange(testCfg *testConfigValues, adapterMap map[openrtb_ext.Bid
 	testExchange := exchange.NewExchange(adapterMap,
 		&wellBehavedCache{},
 		cfg,
+		requestValidator,
 		nil,
 		met,
 		bidderInfos,
@@ -1259,6 +1261,7 @@ func buildTestEndpoint(test testCase, cfg *config.Configuration) (httprouter.Han
 	disableBidders(test.Config.DisabledAdapters, bidderInfos)
 	bidderMap := exchange.GetActiveBidders(bidderInfos)
 	disabledBidders := exchange.GetDisabledBidderWarningMessages(bidderInfos)
+	requestValidator := ortb.NewRequestValidator(bidderMap, disabledBidders, paramValidator)
 	met := &metricsConfig.NilMetricsEngine{}
 	mockFetcher := empty_fetcher.EmptyFetcher{}
 
@@ -1274,7 +1277,7 @@ func buildTestEndpoint(test testCase, cfg *config.Configuration) (httprouter.Han
 	}
 	mockCurrencyRatesServer := httptest.NewServer(http.HandlerFunc(mockCurrencyConversionService.handle))
 
-	testExchange, mockBidServersArray := buildTestExchange(test.Config, adapterMap, mockBidServersArray, mockCurrencyRatesServer, bidderInfos, cfg, met, mockFetcher)
+	testExchange, mockBidServersArray := buildTestExchange(test.Config, adapterMap, mockBidServersArray, mockCurrencyRatesServer, bidderInfos, cfg, met, mockFetcher, requestValidator)
 
 	var storedRequestFetcher stored_requests.Fetcher
 	if len(test.StoredRequest) > 0 {
@@ -1303,7 +1306,7 @@ func buildTestEndpoint(test testCase, cfg *config.Configuration) (httprouter.Han
 		planBuilder = hooks.EmptyPlanBuilder{}
 	}
 
-	var endpointBuilder func(uuidutil.UUIDGenerator, exchange.Exchange, openrtb_ext.BidderParamValidator, stored_requests.Fetcher, stored_requests.AccountFetcher, *config.Configuration, metrics.MetricsEngine, analytics.Runner, map[string]string, []byte, map[string]openrtb_ext.BidderName, stored_requests.Fetcher, hooks.ExecutionPlanBuilder, *exchange.TmaxAdjustmentsPreprocessed) (httprouter.Handle, error)
+	var endpointBuilder func(uuidutil.UUIDGenerator, exchange.Exchange, ortb.RequestValidator, stored_requests.Fetcher, stored_requests.AccountFetcher, *config.Configuration, metrics.MetricsEngine, analytics.Runner, map[string]string, []byte, map[string]openrtb_ext.BidderName, stored_requests.Fetcher, hooks.ExecutionPlanBuilder, *exchange.TmaxAdjustmentsPreprocessed) (httprouter.Handle, error)
 
 	switch test.endpointType {
 	case AMP_ENDPOINT:
@@ -1315,7 +1318,7 @@ func buildTestEndpoint(test testCase, cfg *config.Configuration) (httprouter.Han
 	endpoint, err := endpointBuilder(
 		fakeUUIDGenerator{},
 		testExchange,
-		paramValidator,
+		requestValidator,
 		storedRequestFetcher,
 		accountFetcher,
 		cfg,
diff --git a/endpoints/openrtb2/video_auction_test.go b/endpoints/openrtb2/video_auction_test.go
index cc522c91..55fc7fab 100644
--- a/endpoints/openrtb2/video_auction_test.go
+++ b/endpoints/openrtb2/video_auction_test.go
@@ -20,6 +20,7 @@ import (
 	"github.com/prebid/prebid-server/v2/metrics"
 	metricsConfig "github.com/prebid/prebid-server/v2/metrics/config"
 	"github.com/prebid/prebid-server/v2/openrtb_ext"
+	"github.com/prebid/prebid-server/v2/ortb"
 	"github.com/prebid/prebid-server/v2/prebid_cache_client"
 	"github.com/prebid/prebid-server/v2/privacy"
 	"github.com/prebid/prebid-server/v2/stored_requests/backends/empty_fetcher"
@@ -1236,7 +1237,7 @@ func mockDepsWithMetrics(t *testing.T, ex *mockExchangeVideo) (*endpointDeps, *m
 	deps := &endpointDeps{
 		fakeUUIDGenerator{},
 		ex,
-		mockBidderParamValidator{},
+		ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}),
 		&mockVideoStoredReqFetcher{},
 		&mockVideoStoredReqFetcher{},
 		&mockAccountFetcher{data: mockVideoAccountData},
@@ -1285,7 +1286,7 @@ func mockDeps(t *testing.T, ex *mockExchangeVideo) *endpointDeps {
 	return &endpointDeps{
 		fakeUUIDGenerator{},
 		ex,
-		mockBidderParamValidator{},
+		ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}),
 		&mockVideoStoredReqFetcher{},
 		&mockVideoStoredReqFetcher{},
 		&mockAccountFetcher{data: mockVideoAccountData},
@@ -1310,7 +1311,7 @@ func mockDepsAppendBidderNames(t *testing.T, ex *mockExchangeAppendBidderNames)
 	deps := &endpointDeps{
 		fakeUUIDGenerator{},
 		ex,
-		mockBidderParamValidator{},
+		ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}),
 		&mockVideoStoredReqFetcher{},
 		&mockVideoStoredReqFetcher{},
 		empty_fetcher.EmptyFetcher{},
@@ -1337,7 +1338,7 @@ func mockDepsNoBids(t *testing.T, ex *mockExchangeVideoNoBids) *endpointDeps {
 	edep := &endpointDeps{
 		fakeUUIDGenerator{},
 		ex,
-		mockBidderParamValidator{},
+		ortb.NewRequestValidator(openrtb_ext.BuildBidderMap(), map[string]string{}, mockBidderParamValidator{}),
 		&mockVideoStoredReqFetcher{},
 		&mockVideoStoredReqFetcher{},
 		empty_fetcher.EmptyFetcher{},
diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go
index 5849dbc3..162f4255 100644
--- a/exchange/exchange_test.go
+++ b/exchange/exchange_test.go
@@ -35,10 +35,12 @@ import (
 	metricsConf "github.com/prebid/prebid-server/v2/metrics/config"
 	metricsConfig "github.com/prebid/prebid-server/v2/metrics/config"
 	"github.com/prebid/prebid-server/v2/openrtb_ext"
+	"github.com/prebid/prebid-server/v2/ortb"
 	pbc "github.com/prebid/prebid-server/v2/prebid_cache_client"
 	"github.com/prebid/prebid-server/v2/privacy"
 	"github.com/prebid/prebid-server/v2/stored_requests"
 	"github.com/prebid/prebid-server/v2/stored_requests/backends/file_fetcher"
+	"github.com/prebid/prebid-server/v2/stored_responses"
 	"github.com/prebid/prebid-server/v2/usersync"
 	"github.com/prebid/prebid-server/v2/util/jsonutil"
 	"github.com/prebid/prebid-server/v2/util/ptrutil"
@@ -82,7 +84,7 @@ func TestNewExchange(t *testing.T) {
 		},
 	}.Builder
 
-	e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange)
+	e := NewExchange(adapters, nil, cfg, &mockRequestValidator{}, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange)
 	for _, bidderName := range knownAdapters {
 		if _, ok := e.adapterMap[bidderName]; !ok {
 			if biddersInfo[string(bidderName)].IsEnabled() {
@@ -132,7 +134,7 @@ func TestCharacterEscape(t *testing.T) {
 		},
 	}.Builder
 
-	e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange)
+	e := NewExchange(adapters, nil, cfg, &mockRequestValidator{}, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange)
 
 	// 	3) Build all the parameters e.buildBidResponse(ctx.Background(), liveA... ) needs
 	//liveAdapters []openrtb_ext.BidderName,
@@ -1235,7 +1237,7 @@ func TestGetBidCacheInfoEndToEnd(t *testing.T) {
 		},
 	}.Builder
 
-	e := NewExchange(adapters, pbc, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange)
+	e := NewExchange(adapters, pbc, cfg, &mockRequestValidator{}, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange)
 	// 	3) Build all the parameters e.buildBidResponse(ctx.Background(), liveA... ) needs
 	liveAdapters := []openrtb_ext.BidderName{bidderName}
 
@@ -1594,7 +1596,7 @@ func TestBidResponseCurrency(t *testing.T) {
 		},
 	}.Builder
 
-	e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange)
+	e := NewExchange(adapters, nil, cfg, &mockRequestValidator{}, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange)
 
 	liveAdapters := make([]openrtb_ext.BidderName, 1)
 	liveAdapters[0] = "appnexus"
@@ -1742,7 +1744,7 @@ func TestBidResponseImpExtInfo(t *testing.T) {
 		t.Fatalf("Error intializing adapters: %v", adaptersErr)
 	}
 
-	e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, nil, gdprPermsBuilder, nil, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange)
+	e := NewExchange(adapters, nil, cfg, &mockRequestValidator{}, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, nil, gdprPermsBuilder, nil, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange)
 
 	liveAdapters := make([]openrtb_ext.BidderName, 1)
 	liveAdapters[0] = "appnexus"
@@ -1836,7 +1838,7 @@ func TestRaceIntegration(t *testing.T) {
 		},
 	}.Builder
 
-	ex := NewExchange(adapters, &wellBehavedCache{}, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, &nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange)
+	ex := NewExchange(adapters, &wellBehavedCache{}, cfg, &mockRequestValidator{}, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, &nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange)
 	_, err = ex.HoldAuction(context.Background(), auctionRequest, &debugLog)
 	if err != nil {
 		t.Errorf("HoldAuction returned unexpected error: %v", err)
@@ -1934,7 +1936,7 @@ func TestPanicRecovery(t *testing.T) {
 		},
 	}.Builder
 
-	e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange)
+	e := NewExchange(adapters, nil, cfg, &mockRequestValidator{}, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange)
 
 	chBids := make(chan *bidResponseWrapper, 1)
 	panicker := func(bidderRequest BidderRequest, conversions currency.Conversions) {
@@ -2004,7 +2006,7 @@ func TestPanicRecoveryHighLevel(t *testing.T) {
 			allowAllBidders: true,
 		},
 	}.Builder
-	e := NewExchange(adapters, &mockCache{}, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, categoriesFetcher, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange)
+	e := NewExchange(adapters, &mockCache{}, cfg, &mockRequestValidator{}, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, categoriesFetcher, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange)
 
 	e.adapterMap[openrtb_ext.BidderBeachfront] = panicingAdapter{}
 	e.adapterMap[openrtb_ext.BidderAppnexus] = panicingAdapter{}
@@ -2335,7 +2337,8 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) {
 }
 
 func findBiddersInAuction(t *testing.T, context string, req *openrtb2.BidRequest) []string {
-	if splitImps, err := splitImps(req.Imp); err != nil {
+
+	if splitImps, err := splitImps(req.Imp, &mockRequestValidator{}, nil, false, nil); err != nil {
 		t.Errorf("%s: Failed to parse Bidders from request: %v", context, err)
 		return nil
 	} else {
@@ -2384,6 +2387,8 @@ func newExchangeForTests(t *testing.T, filename string, expectations map[string]
 				mockResponses: map[string]bidderResponse{string(bidderName): spec.MockResponse},
 			}
 			bidderInfos[string(bidderName)] = config.BidderInfo{ModifyingVastXmlAllowed: spec.ModifyingVastXmlAllowed}
+		} else {
+			bidderInfos[string(bidderName)] = config.BidderInfo{}
 		}
 	}
 
@@ -2433,6 +2438,12 @@ func newExchangeForTests(t *testing.T, filename string, expectations map[string]
 	}
 
 	metricsEngine := metricsConf.NewMetricsEngine(&config.Configuration{}, openrtb_ext.CoreBidderNames(), nil, nil)
+	paramValidator, err := openrtb_ext.NewBidderParamsValidator("../static/bidder-params")
+	if err != nil {
+		t.Fatalf("Failed to create params validator: %v", error)
+	}
+	bidderMap := GetActiveBidders(bidderInfos)
+	requestValidator := ortb.NewRequestValidator(bidderMap, map[string]string{}, paramValidator)
 	requestSplitter := requestSplitter{
 		bidderToSyncerKey: bidderToSyncerKey,
 		me:                metricsEngine,
@@ -2440,6 +2451,7 @@ func newExchangeForTests(t *testing.T, filename string, expectations map[string]
 		gdprPermsBuilder:  gdprPermsBuilder,
 		hostSChainNode:    hostSChainNode,
 		bidderInfo:        bidderInfos,
+		requestValidator:  requestValidator,
 	}
 
 	return &exchange{
@@ -4565,7 +4577,7 @@ func TestPassExperimentConfigsToHoldAuction(t *testing.T) {
 		},
 	}.Builder
 
-	e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &signer, macros.NewStringIndexBasedReplacer(), nil).(*exchange)
+	e := NewExchange(adapters, nil, cfg, &mockRequestValidator{}, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &signer, macros.NewStringIndexBasedReplacer(), nil).(*exchange)
 
 	// Define mock incoming bid requeset
 	mockBidRequest := &openrtb2.BidRequest{
@@ -6332,3 +6344,11 @@ func TestBidsToUpdate(t *testing.T) {
 		})
 	}
 }
+
+type mockRequestValidator struct {
+	errors []error
+}
+
+func (mrv *mockRequestValidator) ValidateImp(imp *openrtb_ext.ImpWrapper, index int, aliases map[string]string, hasStoredResponses bool, storedBidResponses stored_responses.ImpBidderStoredResp) []error {
+	return mrv.errors
+}
diff --git a/exchange/exchangetest/imp-fpd-invalid.json b/exchange/exchangetest/imp-fpd-invalid.json
new file mode 100644
index 00000000..6d0bd9b9
--- /dev/null
+++ b/exchange/exchangetest/imp-fpd-invalid.json
@@ -0,0 +1,55 @@
+{
+    "requestType": "openrtb2-web",
+    "incomingRequest": {
+        "ortbRequest": {
+            "id": "some-request-id",
+            "site": {
+                "page": "test.somepage.com"
+            },
+            "imp": [
+                {
+                    "id": "imp-id-1",
+                    "banner": {
+                        "format": [
+                            {
+                                "w": 300,
+                                "h": 600
+                            }
+                        ]
+                    },
+                    "ext": {
+                        "prebid": {
+                            "bidder": {
+                                "appnexus": {
+                                    "placementId": 1
+                                },
+                                "audienceNetwork": {
+                                    "placementId": "some-placement"
+                                }
+                            },
+                            "imp": {
+                                "appnexus": {
+                                    "id": "imp-id-1-appnexus"
+                                },
+                                "audienceNetwork": {
+                                    "banner": {
+                                        "format": [
+                                            {
+                                                "w": -1,
+                                                "h": -1
+                                            }
+                                        ]
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            ]
+        }
+    },
+    "outgoingRequests": {},
+    "response": {
+        "error": "merging bidder imp first party data for imp imp-id-1 results in an invalid imp: [request.imp[0].banner.format[0].w must be a positive number]"
+    }
+}
\ No newline at end of file
diff --git a/exchange/exchangetest/imp-fpd-multiple-bidders.json b/exchange/exchangetest/imp-fpd-multiple-bidders.json
new file mode 100644
index 00000000..411435d9
--- /dev/null
+++ b/exchange/exchangetest/imp-fpd-multiple-bidders.json
@@ -0,0 +1,202 @@
+{
+    "requestType": "openrtb2-web",
+    "incomingRequest": {
+        "ortbRequest": {
+            "id": "some-request-id",
+            "site": {
+                "page": "test.somepage.com"
+            },
+            "imp": [
+                {
+                    "id": "imp-id-1",
+                    "banner": {
+                        "format": [
+                            {
+                                "w": 300,
+                                "h": 600
+                            }
+                        ]
+                    },
+                    "ext": {
+                        "prebid": {
+                            "bidder": {
+                                "appnexus": {
+                                    "placementId": 1
+                                },
+                                "audienceNetwork": {
+                                    "placementId": "some-placement"
+                                }
+                            },
+                            "imp": {
+                                "appnexus": {
+                                    "id": "imp-id-1-appnexus"
+                                },
+                                "audienceNetwork": {
+                                    "banner": {
+                                        "format": [
+                                            {
+                                                "w": 3000,
+                                                "h": 6000
+                                            }
+                                        ]
+                                    }
+                                },
+                                "rubicon": {
+                                    "banner": {
+                                        "w": 150,
+                                        "h": 300
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            ]
+        }
+    },
+    "outgoingRequests": {
+        "appnexus": {
+            "expectRequest": {
+                "ortbRequest": {
+                    "id": "some-request-id",
+                    "site": {
+                        "page": "test.somepage.com"
+                    },
+                    "imp": [
+                        {
+                            "id": "imp-id-1-appnexus",
+                            "banner": {
+                                "format": [
+                                    {
+                                        "w": 300,
+                                        "h": 600
+                                    }
+                                ]
+                            },
+                            "ext": {
+                                "bidder": {
+                                    "placementId": 1
+                                }
+                            }
+                        }
+                    ]
+                }
+            },
+            "mockResponse": {
+                "pbsSeatBids": [
+                    {
+                        "pbsBids": [
+                            {
+                                "ortbBid": {
+                                    "id": "apn-bid",
+                                    "impid": "imp-id-1-appnexus",
+                                    "price": 0.3,
+                                    "w": 300,
+                                    "h": 600,
+                                    "crid": "creative-1"
+                                },
+                                "bidType": "banner"
+                            }
+                        ],
+                        "seat": "appnexus"
+                    }
+                ]
+            }
+        },
+        "audienceNetwork": {
+            "expectRequest": {
+                "ortbRequest": {
+                    "id": "some-request-id",
+                    "site": {
+                        "page": "test.somepage.com"
+                    },
+                    "imp": [
+                        {
+                            "id": "imp-id-1",
+                            "banner": {
+                                "format": [
+                                    {
+                                        "w": 3000,
+                                        "h": 6000
+                                    }
+                                ]
+                            },
+                            "ext": {
+                                "bidder": {
+                                    "placementId": "some-placement"
+                                }
+                            }
+                        }
+                    ]
+                }
+            },
+            "mockResponse": {
+                "pbsSeatBids": [
+                    {
+                        "pbsBids": [
+                            {
+                                "ortbBid": {
+                                    "id": "apn-bid",
+                                    "impid": "imp-id-1",
+                                    "price": 0.3,
+                                    "w": 3000,
+                                    "h": 6000,
+                                    "crid": "creative-1"
+                                },
+                                "bidType": "banner"
+                            }
+                        ],
+                        "seat": "audiencenetwork"
+                    }
+                ]
+            }
+        }
+    },
+    "response": {
+        "bids": {
+            "id": "some-request-id",
+            "seatbid": [
+                {
+                    "seat": "appnexus",
+                    "bid": [
+                        {
+                            "id": "apn-bid",
+                            "impid": "imp-id-1-appnexus",
+                            "price": 0.3,
+                            "w": 300,
+                            "h": 600,
+                            "crid": "creative-1",
+                            "ext": {
+                                "origbidcpm": 0.3,
+                                "prebid": {
+                                    "meta": {},
+                                    "type": "banner"
+                                }
+                            }
+                        }
+                    ]
+                },
+                {
+                    "seat": "audiencenetwork",
+                    "bid": [
+                        {
+                            "id": "apn-bid",
+                            "impid": "imp-id-1",
+                            "price": 0.3,
+                            "w": 3000,
+                            "h": 6000,
+                            "crid": "creative-1",
+                            "ext": {
+                                "origbidcpm": 0.3,
+                                "prebid": {
+                                    "meta": {},
+                                    "type": "banner"
+                                }
+                            }
+                        }
+                    ]
+                }
+            ]
+        }
+    }
+}
\ No newline at end of file
diff --git a/exchange/exchangetest/imp-fpd-multiple-imp.json b/exchange/exchangetest/imp-fpd-multiple-imp.json
new file mode 100644
index 00000000..902a071b
--- /dev/null
+++ b/exchange/exchangetest/imp-fpd-multiple-imp.json
@@ -0,0 +1,288 @@
+{
+    "requestType": "openrtb2-web",
+    "incomingRequest": {
+        "ortbRequest": {
+            "id": "some-request-id",
+            "site": {
+                "page": "test.somepage.com"
+            },
+            "imp": [
+                {
+                    "id": "imp-id-1",
+                    "banner": {
+                        "w": 100,
+                        "format": [
+                            {
+                                "w": 300,
+                                "h": 600
+                            }
+                        ]
+                    },
+                    "ext": {
+                        "prebid": {
+                            "bidder": {
+                                "appnexus": {
+                                    "placementId": 1
+                                }
+                            },
+                            "imp": {
+                                "appnexus": {
+                                    "banner": {
+                                        "h": 200,
+                                        "format": [
+                                            {
+                                                "w": 3000,
+                                                "h": 6000
+                                            }
+                                        ],
+                                        "btype": [1,2]
+                                    }
+                                }
+                            }
+                        }
+                    }
+                },
+                {
+                    "id": "imp-id-2",
+                    "video": {
+                        "mimes": [
+                            "video/mp3"
+                        ],
+                        "w": 200
+                    },
+                    "ext": {
+                        "prebid": {
+                            "bidder": {
+                                "appnexus": {
+                                    "placementId": 1
+                                }
+                            },
+                            "imp": {
+                                "appnexus": {
+                                    "video": {
+                                        "mimes": [
+                                            "video/mp4"
+                                        ],
+                                        "h": 400
+                                    }
+                                }
+                            }
+                        }
+                    }
+                },
+                {
+                    "id": "imp-id-3",
+                    "audio": {
+                        "mimes": ["audio/mp4"],
+                        "minduration": 1,
+                        "maxduration": 10,
+                        "durfloors": [{
+                            "mindur": 1,
+                            "maxdur": 10,
+                            "bidfloor": 10.0
+                        }]
+                    },
+                    "native": {
+                        "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmtcnt\":6,\"plcmttype\":4,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"wmin\":492,\"hmin\":328,\"type\":3,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":4,\"data\":{\"type\":6}},{\"id\":5,\"data\":{\"type\":7}},{\"id\":6,\"data\":{\"type\":1,\"len\":20}}]}",
+                        "ver": "1.1"
+                    },
+                    "ext": {
+                        "prebid": {
+                            "bidder": {
+                                "appnexus": {
+                                    "placementId": 1
+                                }
+                            },
+                            "imp": {
+                                "appnexus": {
+                                    "audio": {
+                                        "maxduration": 20,
+                                        "durfloors": [{
+                                            "maxdur": 30
+                                        }],
+                                        "poddur": 40
+                                    },
+                                    "native": {
+                                        "ver": "2.2",
+                                        "ctype": 1
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            ]
+        }
+    },
+    "outgoingRequests": {
+        "appnexus": {
+            "expectRequest": {
+                "ortbRequest": {
+                    "id": "some-request-id",
+                    "site": {
+                        "page": "test.somepage.com"
+                    },
+                    "imp": [
+                        {
+                            "id": "imp-id-1",
+                            "banner": {
+                                "w": 100,
+                                "h": 200,
+                                "format": [
+                                    {
+                                        "w": 3000,
+                                        "h": 6000
+                                    }
+                                ],
+                                "btype": [1,2]
+                            },
+                            "ext": {
+                                "bidder": {
+                                    "placementId": 1
+                                }
+                            }
+                        },
+                        {
+                            "id": "imp-id-2",
+                            "video": {
+                                "mimes": [
+                                    "video/mp4"
+                                ],
+                                "w": 200,
+                                "h": 400
+                            },
+                            "ext": {
+                                "bidder": {
+                                    "placementId": 1
+                                }
+                            }
+                        },
+                        {
+                            "id": "imp-id-3",
+                            "audio": {
+                                "mimes": ["audio/mp4"],
+                                "minduration": 1,
+                                "maxduration": 20,
+                                "durfloors": [{
+                                    "mindur": 1,
+                                    "maxdur": 30,
+                                    "bidfloor": 10.0
+                                }],
+                                "poddur": 40
+                            },
+                            "native": {
+                                "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmttype\":4,\"plcmtcnt\":6,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":492,\"hmin\":328,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":4,\"data\":{\"type\":6}},{\"id\":5,\"data\":{\"type\":7}},{\"id\":6,\"data\":{\"type\":1,\"len\":20}}]}",
+                                "ver": "2.2",
+                                "ctype": 1
+                            },
+                            "ext": {
+                                "bidder": {
+                                    "placementId": 1
+                                }
+                            }
+                        }
+                    ]
+                }
+            },
+            "mockResponse": {
+                "pbsSeatBids": [
+                    {
+                        "pbsBids": [
+                            {
+                                "ortbBid": {
+                                    "id": "apn-bid-1",
+                                    "impid": "imp-id-1",
+                                    "price": 0.1,
+                                    "w": 3000,
+                                    "h": 6000,
+                                    "crid": "creative-1"
+                                },
+                                "bidType": "banner"
+                            },
+                            {
+                                "ortbBid": {
+                                    "id": "apn-bid-2",
+                                    "impid": "imp-id-2",
+                                    "price": 0.2,
+                                    "w": 200,
+                                    "h": 400,
+                                    "crid": "creative-2"
+                                },
+                                "bidType": "video"
+                            },
+                            {
+                                "ortbBid": {
+                                    "id": "apn-bid-3",
+                                    "impid": "imp-id-3",
+                                    "price": 0.3,
+                                    "w": 300,
+                                    "h": 600,
+                                    "crid": "creative-3"
+                                },
+                                "bidType": "native"
+                            }
+                        ],
+                        "seat": "appnexus"
+                    }
+                ]
+            }
+        }
+    },
+    "response": {
+        "bids": {
+            "id": "some-request-id",
+            "seatbid": [
+                {
+                    "seat": "appnexus",
+                    "bid": [
+                        {
+                            "id": "apn-bid-1",
+                            "impid": "imp-id-1",
+                            "price": 0.1,
+                            "w": 3000,
+                            "h": 6000,
+                            "crid": "creative-1",
+                            "ext": {
+                                "origbidcpm": 0.1,
+                                "prebid": {
+                                    "meta": {},
+                                    "type": "banner"
+                                }
+                            }
+                        },
+                        {
+                            "id": "apn-bid-2",
+                            "impid": "imp-id-2",
+                            "price": 0.2,
+                            "w": 200,
+                            "h": 400,
+                            "crid": "creative-2",
+                            "ext": {
+                                "origbidcpm": 0.2,
+                                "prebid": {
+                                    "meta": {},
+                                    "type": "video"
+                                }
+                            }
+                        },
+                        {
+                            "id": "apn-bid-3",
+                            "impid": "imp-id-3",
+                            "price": 0.3,
+                            "w": 300,
+                            "h": 600,
+                            "crid": "creative-3",
+                            "ext": {
+                                "origbidcpm": 0.3,
+                                "prebid": {
+                                    "meta": {},
+                                    "type": "native"
+                                }
+                            }
+                        }
+                    ]
+                }
+            ]
+        }
+    }
+}
\ No newline at end of file
diff --git a/exchange/utils_test.go b/exchange/utils_test.go
index 69195736..c22fc091 100644
--- a/exchange/utils_test.go
+++ b/exchange/utils_test.go
@@ -93,10 +93,11 @@ func assertReq(t *testing.T, bidderRequests []BidderRequest,
 
 func TestSplitImps(t *testing.T) {
 	testCases := []struct {
-		description   string
-		givenImps     []openrtb2.Imp
-		expectedImps  map[string][]openrtb2.Imp
-		expectedError string
+		description     string
+		givenImps       []openrtb2.Imp
+		validatorErrors []error
+		expectedImps    map[string][]openrtb2.Imp
+		expectedError   string
 	}{
 		{
 			description:   "Nil",
@@ -209,10 +210,105 @@ func TestSplitImps(t *testing.T) {
 			},
 			expectedError: "invalid json for imp[0]: do not know how to skip: 109",
 		},
+		{
+			description: "Malformed imp.ext.prebid.imp",
+			givenImps: []openrtb2.Imp{
+				{ID: "imp1", Ext: json.RawMessage(`{"prebid": {"imp": malformed}}`)},
+			},
+			expectedError: "invalid json for imp[0]: do not know how to skip: 109",
+		},
+		{
+			description: "valid FPD at imp.ext.prebid.imp for valid bidder",
+			givenImps: []openrtb2.Imp{
+				{
+					ID: "imp1",
+					Banner: &openrtb2.Banner{
+						Format: []openrtb2.Format{
+							{
+								W: 10,
+								H: 20,
+							},
+						},
+					},
+					Ext: json.RawMessage(`{"prebid":{"bidder":{"bidderA":{"imp1paramA":"imp1valueA"}},"imp":{"bidderA":{"id":"impFPD", "banner":{"format":[{"h":30}]}}}}}`),
+				},
+			},
+			expectedImps: map[string][]openrtb2.Imp{
+				"bidderA": {
+					{
+						ID: "impFPD",
+						Banner: &openrtb2.Banner{
+							Format: []openrtb2.Format{
+								{
+									W: 10,
+									H: 30,
+								},
+							},
+						},
+						Ext: json.RawMessage(`{"bidder":{"imp1paramA":"imp1valueA"}}`),
+					},
+				},
+			},
+			expectedError: "",
+		},
+		{
+			description: "valid FPD at imp.ext.prebid.imp for unknown bidder",
+			givenImps: []openrtb2.Imp{
+				{
+					ID: "imp1",
+					Banner: &openrtb2.Banner{
+						Format: []openrtb2.Format{
+							{
+								W: 10,
+								H: 20,
+							},
+						},
+					},
+					Ext: json.RawMessage(`{"prebid":{"bidder":{"bidderB":{"imp1paramB":"imp1valueB"}},"imp":{"bidderA":{"id":"impFPD", "banner":{"format":[{"h":30}]}}}}}`),
+				},
+			},
+			expectedImps: map[string][]openrtb2.Imp{
+				"bidderB": {
+					{
+						ID: "imp1",
+						Banner: &openrtb2.Banner{
+							Format: []openrtb2.Format{
+								{
+									W: 10,
+									H: 20,
+								},
+							},
+						},
+						Ext: json.RawMessage(`{"bidder":{"imp1paramB":"imp1valueB"}}`),
+					},
+				},
+			},
+			expectedError: "",
+		},
+		{
+			description: "invalid FPD at imp.ext.prebid.imp for valid bidder",
+			givenImps: []openrtb2.Imp{
+				{
+					ID: "imp1",
+					Banner: &openrtb2.Banner{
+						Format: []openrtb2.Format{
+							{
+								W: 10,
+								H: 20,
+							},
+						},
+					},
+					Ext: json.RawMessage(`{"prebid":{"bidder":{"bidderA":{"imp1paramA":"imp1valueA"}},"imp":{"bidderA":{"id":"impFPD", "banner":{"format":[{"w":0,"h":0}]}}}}}`),
+				},
+			},
+			validatorErrors: []error{errors.New("some error")},
+			expectedImps:    nil,
+			expectedError:   "merging bidder imp first party data for imp imp1 results in an invalid imp: [some error]",
+		},
 	}
 
 	for _, test := range testCases {
-		imps, err := splitImps(test.givenImps)
+		imps, err := splitImps(test.givenImps, &mockRequestValidator{errors: test.validatorErrors}, nil, false, nil)
 
 		if test.expectedError == "" {
 			assert.NoError(t, err, test.description+":err")
@@ -224,6 +320,201 @@ func TestSplitImps(t *testing.T) {
 	}
 }
 
+func TestMergeImpFPD(t *testing.T) {
+	imp1 := &openrtb2.Imp{
+		ID: "imp1",
+		Banner: &openrtb2.Banner{
+			W: ptrutil.ToPtr[int64](200),
+			H: ptrutil.ToPtr[int64](400),
+		},
+	}
+
+	tests := []struct {
+		description string
+		imp         *openrtb2.Imp
+		fpd         json.RawMessage
+		wantImp     *openrtb2.Imp
+		wantError   bool
+	}{
+		{
+			description: "nil",
+			imp:         nil,
+			fpd:         nil,
+			wantImp:     nil,
+			wantError:   true,
+		},
+		{
+			description: "nil_fpd",
+			imp:         imp1,
+			fpd:         nil,
+			wantImp:     imp1,
+			wantError:   true,
+		},
+		{
+			description: "empty_fpd",
+			imp:         imp1,
+			fpd:         json.RawMessage(`{}`),
+			wantImp:     imp1,
+			wantError:   false,
+		},
+		{
+			description: "nil_imp",
+			imp:         nil,
+			fpd:         json.RawMessage(`{}`),
+			wantImp:     nil,
+			wantError:   true,
+		},
+		{
+			description: "zero_value_imp",
+			imp:         &openrtb2.Imp{},
+			fpd:         json.RawMessage(`{}`),
+			wantImp:     &openrtb2.Imp{},
+			wantError:   false,
+		},
+		{
+			description: "invalid_json_on_existing_imp",
+			imp: &openrtb2.Imp{
+				Ext: json.RawMessage(`malformed`),
+			},
+			fpd: json.RawMessage(`{"ext": {"a":1}}`),
+			wantImp: &openrtb2.Imp{
+				Ext: json.RawMessage(`malformed`),
+			},
+			wantError: true,
+		},
+		{
+			description: "invalid_json_in_fpd",
+			imp: &openrtb2.Imp{
+				Ext: json.RawMessage(`{"ext": {"a":1}}`),
+			},
+			fpd: json.RawMessage(`malformed`),
+			wantImp: &openrtb2.Imp{
+				Ext: json.RawMessage(`{"ext": {"a":1}}`),
+			},
+			wantError: true,
+		},
+		{
+			description: "override_everything",
+			imp: &openrtb2.Imp{
+				ID:     "id1",
+				Metric: []openrtb2.Metric{{Type: "type1", Value: 1, Vendor: "vendor1"}},
+				Banner: &openrtb2.Banner{
+					W: ptrutil.ToPtr[int64](1),
+					H: ptrutil.ToPtr[int64](2),
+					Format: []openrtb2.Format{
+						{
+							W:   10,
+							H:   20,
+							Ext: json.RawMessage(`{"formatkey1":"formatval1"}`),
+						},
+					},
+				},
+				Instl:    1,
+				BidFloor: 1,
+				Ext:      json.RawMessage(`{"cool":"test"}`),
+			},
+			fpd: json.RawMessage(`{"id": "id2", "metric": [{"type":"type2", "value":2, "vendor":"vendor2"}], "banner": {"w":100, "h": 200, "format": [{"w":1000, "h":2000, "ext":{"formatkey1":"formatval2"}}]}, "instl":2, "bidfloor":2, "ext":{"cool":"test2"} }`),
+			wantImp: &openrtb2.Imp{
+				ID:     "id2",
+				Metric: []openrtb2.Metric{{Type: "type2", Value: 2, Vendor: "vendor2"}},
+				Banner: &openrtb2.Banner{
+					W: ptrutil.ToPtr[int64](100),
+					H: ptrutil.ToPtr[int64](200),
+					Format: []openrtb2.Format{
+						{
+							W:   1000,
+							H:   2000,
+							Ext: json.RawMessage(`{"formatkey1":"formatval2"}`),
+						},
+					},
+				},
+				Instl:    2,
+				BidFloor: 2,
+				Ext:      json.RawMessage(`{"cool":"test2"}`),
+			},
+		},
+		{
+			description: "override_partial_simple",
+			imp:         imp1,
+			fpd:         json.RawMessage(`{"id": "456", "banner": {"format": [{"w":1, "h":2}]} }`),
+			wantImp: &openrtb2.Imp{
+				ID: "456",
+				Banner: &openrtb2.Banner{
+					W: ptrutil.ToPtr[int64](200),
+					H: ptrutil.ToPtr[int64](400),
+					Format: []openrtb2.Format{
+						{
+							W: 1,
+							H: 2,
+						},
+					},
+				},
+			},
+		},
+		{
+			description: "override_partial_complex",
+			imp: &openrtb2.Imp{
+				ID:     "id1",
+				Metric: []openrtb2.Metric{{Type: "type1", Value: 1, Vendor: "vendor1"}},
+				Banner: &openrtb2.Banner{
+					W: ptrutil.ToPtr[int64](1),
+					H: ptrutil.ToPtr[int64](2),
+					Format: []openrtb2.Format{
+						{
+							W:   10,
+							H:   20,
+							Ext: json.RawMessage(`{"formatkey1":"formatval1", "formatkey2":"formatval2"}`),
+						},
+					},
+				},
+				Instl:        1,
+				TagID:        "tag1",
+				BidFloor:     1,
+				Rwdd:         1,
+				DT:           1,
+				IframeBuster: []string{"buster1", "buster2"},
+				Ext:          json.RawMessage(`{"cool1":"test1", "cool2":"test2"}`),
+			},
+			fpd: json.RawMessage(`{"id": "id2", "metric": [{"type":"type2", "value":2, "vendor":"vendor2"}], "banner": {"w":100, "format": [{"w":1000, "ext":{"formatkey1":"formatval11"}}]}, "instl":2, "bidfloor":2, "ext":{"cool1":"test11"} }`),
+			wantImp: &openrtb2.Imp{
+				ID:     "id2",
+				Metric: []openrtb2.Metric{{Type: "type2", Value: 2, Vendor: "vendor2"}},
+				Banner: &openrtb2.Banner{
+					W: ptrutil.ToPtr[int64](100),
+					H: ptrutil.ToPtr[int64](2),
+					Format: []openrtb2.Format{
+						{
+							W:   1000,
+							H:   20,
+							Ext: json.RawMessage(`{"formatkey1":"formatval11","formatkey2":"formatval2"}`),
+						},
+					},
+				},
+				Instl:        2,
+				TagID:        "tag1",
+				BidFloor:     2,
+				Rwdd:         1,
+				DT:           1,
+				IframeBuster: []string{"buster1", "buster2"},
+				Ext:          json.RawMessage(`{"cool1":"test11","cool2":"test2"}`),
+			},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.description, func(t *testing.T) {
+			err := mergeImpFPD(test.imp, test.fpd, 1)
+			assert.Equal(t, test.wantImp, test.imp)
+
+			if test.wantError {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
+
 func TestCreateSanitizedImpExt(t *testing.T) {
 	testCases := []struct {
 		description       string
diff --git a/ortb/request_validator_audio_test.go b/ortb/request_validator_audio_test.go
new file mode 100644
index 00000000..9b54f925
--- /dev/null
+++ b/ortb/request_validator_audio_test.go
@@ -0,0 +1,99 @@
+package ortb
+
+import (
+	"testing"
+
+	"github.com/prebid/openrtb/v20/openrtb2"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestValidateAudio(t *testing.T) {
+	tests := []struct {
+		name      string
+		audio     *openrtb2.Audio
+		wantError bool
+	}{
+		{
+			name:      "nil",
+			audio:     nil,
+			wantError: false,
+		},
+		{
+			name: "well_formed",
+			audio: &openrtb2.Audio{
+				MIMEs:      []string{"MIME1"},
+				Sequence:   0,
+				MaxSeq:     0,
+				MinBitrate: 0,
+				MaxBitrate: 0,
+			},
+			wantError: false,
+		},
+		{
+			name: "mimes_is_zero",
+			audio: &openrtb2.Audio{
+				MIMEs:      []string{},
+				Sequence:   0,
+				MaxSeq:     0,
+				MinBitrate: 0,
+				MaxBitrate: 0,
+			},
+			wantError: true,
+		},
+		{
+			name: "negative_sequence",
+			audio: &openrtb2.Audio{
+				MIMEs:      []string{"MIME1"},
+				Sequence:   -1,
+				MaxSeq:     0,
+				MinBitrate: 0,
+				MaxBitrate: 0,
+			},
+			wantError: true,
+		},
+		{
+			name: "negative_max_sequence",
+			audio: &openrtb2.Audio{
+				MIMEs:      []string{"MIME1"},
+				Sequence:   0,
+				MaxSeq:     -1,
+				MinBitrate: 0,
+				MaxBitrate: 0,
+			},
+			wantError: true,
+		},
+		{
+			name: "negative_min_bit_rate",
+			audio: &openrtb2.Audio{
+				MIMEs:      []string{"MIME1"},
+				Sequence:   0,
+				MaxSeq:     0,
+				MinBitrate: -1,
+				MaxBitrate: 0,
+			},
+			wantError: true,
+		},
+		{
+			name: "negative_max_bit_rate",
+			audio: &openrtb2.Audio{
+				MIMEs:      []string{"MIME1"},
+				Sequence:   0,
+				MaxSeq:     0,
+				MinBitrate: 0,
+				MaxBitrate: -1,
+			},
+			wantError: true,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			result := validateAudio(test.audio, 1)
+			if test.wantError {
+				assert.Error(t, result)
+			} else {
+				assert.NoError(t, result)
+			}
+		})
+	}
+}
diff --git a/ortb/request_validator_banner_test.go b/ortb/request_validator_banner_test.go
new file mode 100644
index 00000000..ea3ec3a0
--- /dev/null
+++ b/ortb/request_validator_banner_test.go
@@ -0,0 +1,288 @@
+package ortb
+
+import (
+	"testing"
+
+	"github.com/prebid/openrtb/v20/openrtb2"
+	"github.com/prebid/prebid-server/v2/util/ptrutil"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestValidateBanner(t *testing.T) {
+	tests := []struct {
+		name         string
+		banner       *openrtb2.Banner
+		interstitial bool
+		wantError    bool
+	}{
+		{
+			name:         "nil",
+			banner:       nil,
+			interstitial: false,
+			wantError:    false,
+		},
+		{
+			name:         "no_root_or_format_and_interstitial",
+			banner:       &openrtb2.Banner{},
+			interstitial: true,
+			wantError:    false,
+		},
+		{
+			name:         "no_root_or_format_and_not_interstitial",
+			banner:       &openrtb2.Banner{},
+			interstitial: false,
+			wantError:    true,
+		},
+		{
+			name: "well_formed_root",
+			banner: &openrtb2.Banner{
+				W: ptrutil.ToPtr[int64](11),
+				H: ptrutil.ToPtr[int64](1),
+			},
+			interstitial: false,
+			wantError:    false,
+		},
+		{
+			name: "well_formed_format",
+			banner: &openrtb2.Banner{
+				Format: []openrtb2.Format{
+					{
+						W: 1,
+						H: 1,
+					},
+				},
+			},
+			interstitial: false,
+			wantError:    false,
+		},
+		{
+			name: "invalid_format",
+			banner: &openrtb2.Banner{
+				Format: []openrtb2.Format{
+					{
+						W:      1,
+						H:      1,
+						HRatio: -1,
+					},
+				},
+			},
+			interstitial: false,
+			wantError:    true,
+		},
+		{
+			name: "negative_width",
+			banner: &openrtb2.Banner{
+				W: ptrutil.ToPtr[int64](-1),
+				H: ptrutil.ToPtr[int64](1),
+			},
+			interstitial: false,
+			wantError:    true,
+		},
+		{
+			name: "negative_height",
+			banner: &openrtb2.Banner{
+				W: ptrutil.ToPtr[int64](1),
+				H: ptrutil.ToPtr[int64](-1),
+			},
+			interstitial: false,
+			wantError:    true,
+		},
+		{
+			name: "nonzero_wmin",
+			banner: &openrtb2.Banner{
+				W:    ptrutil.ToPtr[int64](1),
+				H:    ptrutil.ToPtr[int64](1),
+				WMin: 1,
+			},
+			interstitial: false,
+			wantError:    true,
+		},
+		{
+			name: "nonzero_wmax",
+			banner: &openrtb2.Banner{
+				W:    ptrutil.ToPtr[int64](1),
+				H:    ptrutil.ToPtr[int64](1),
+				WMax: 1,
+			},
+			interstitial: false,
+			wantError:    true,
+		},
+		{
+			name: "nonzero_hmin",
+			banner: &openrtb2.Banner{
+				W:    ptrutil.ToPtr[int64](1),
+				H:    ptrutil.ToPtr[int64](1),
+				HMin: 1,
+			},
+			interstitial: false,
+			wantError:    true,
+		},
+		{
+			name: "nonzero_hmax",
+			banner: &openrtb2.Banner{
+				W:    ptrutil.ToPtr[int64](1),
+				H:    ptrutil.ToPtr[int64](1),
+				HMax: 1,
+			},
+			interstitial: false,
+			wantError:    true,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			result := validateBanner(test.banner, 1, test.interstitial)
+			if test.wantError {
+				assert.Error(t, result)
+			} else {
+				assert.NoError(t, result)
+			}
+		})
+	}
+}
+
+func TestValidateFormat(t *testing.T) {
+	tests := []struct {
+		name      string
+		format    *openrtb2.Format
+		wantError bool
+	}{
+		{
+			name:      "nil",
+			format:    nil,
+			wantError: false,
+		},
+		{
+			name: "well_formed",
+			format: &openrtb2.Format{
+				W:      1,
+				H:      1,
+				WRatio: 0,
+				HRatio: 0,
+				WMin:   0,
+			},
+			wantError: false,
+		},
+		{
+			name: "well_formed_using_ratios",
+			format: &openrtb2.Format{
+				W:      0,
+				H:      0,
+				WRatio: 1,
+				HRatio: 1,
+				WMin:   1,
+			},
+			wantError: false,
+		},
+		{
+			name: "negative_width",
+			format: &openrtb2.Format{
+				W:      -1,
+				H:      1,
+				WRatio: 0,
+				HRatio: 0,
+				WMin:   0,
+			},
+			wantError: true,
+		},
+		{
+			name: "negative_height",
+			format: &openrtb2.Format{
+				W:      1,
+				H:      -1,
+				WRatio: 0,
+				HRatio: 0,
+				WMin:   0,
+			},
+			wantError: true,
+		},
+		{
+			name: "negative_width_ratio",
+			format: &openrtb2.Format{
+				W:      0,
+				H:      0,
+				WRatio: -1,
+				HRatio: 1,
+				WMin:   1,
+			},
+			wantError: true,
+		},
+		{
+			name: "negative_height_ratio",
+			format: &openrtb2.Format{
+				W:      0,
+				H:      0,
+				WRatio: 1,
+				HRatio: -1,
+				WMin:   1,
+			},
+			wantError: true,
+		},
+		{
+			name: "negative_height_wmin",
+			format: &openrtb2.Format{
+				W:      1,
+				H:      1,
+				WRatio: 1,
+				HRatio: 1,
+				WMin:   -1,
+			},
+			wantError: true,
+		},
+		{
+			name: "using_both_formats",
+			format: &openrtb2.Format{
+				W:      1,
+				H:      1,
+				WRatio: 1,
+				HRatio: 1,
+				WMin:   1,
+			},
+			wantError: true,
+		},
+		{
+			name: "using_neither_format",
+			format: &openrtb2.Format{
+				W:      0,
+				H:      0,
+				WRatio: 0,
+				HRatio: 0,
+				WMin:   0,
+			},
+			wantError: true,
+		},
+		{
+			name: "using_non_ratios_but_zeros",
+			format: &openrtb2.Format{
+				W:      1,
+				H:      0,
+				WRatio: 0,
+				HRatio: 0,
+				WMin:   0,
+			},
+			wantError: true,
+		},
+		{
+			name: "using_ratios_but_zeros",
+			format: &openrtb2.Format{
+				W:      0,
+				H:      0,
+				WRatio: 1,
+				HRatio: 0,
+				WMin:   0,
+			},
+			wantError: true,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			result := validateFormat(test.format, 1, 1)
+			if test.wantError {
+				assert.Error(t, result)
+			} else {
+				assert.NoError(t, result)
+			}
+		})
+	}
+}
diff --git a/ortb/request_validator_native_test.go b/ortb/request_validator_native_test.go
new file mode 100644
index 00000000..f44cfef3
--- /dev/null
+++ b/ortb/request_validator_native_test.go
@@ -0,0 +1,428 @@
+package ortb
+
+import (
+	"testing"
+
+	"github.com/prebid/openrtb/v20/native1"
+	nativeRequests "github.com/prebid/openrtb/v20/native1/request"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestValidateNativeContextTypes(t *testing.T) {
+	impIndex := 4
+
+	testCases := []struct {
+		description      string
+		givenContextType native1.ContextType
+		givenSubType     native1.ContextSubType
+		expectedError    string
+	}{
+		{
+			description:      "No Types Specified",
+			givenContextType: 0,
+			givenSubType:     0,
+			expectedError:    "",
+		},
+		{
+			description:      "All Types Exchange Specific",
+			givenContextType: 500,
+			givenSubType:     500,
+			expectedError:    "",
+		},
+		{
+			description:      "Context Type Known Value - Sub Type Unspecified",
+			givenContextType: 1,
+			givenSubType:     0,
+			expectedError:    "",
+		},
+		{
+			description:      "Context Type Negative",
+			givenContextType: -1,
+			givenSubType:     0,
+			expectedError:    "request.imp[4].native.request.context is invalid. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39",
+		},
+		{
+			description:      "Context Type Just Above Range",
+			givenContextType: 4, // Range is currently 1-3
+			givenSubType:     0,
+			expectedError:    "request.imp[4].native.request.context is invalid. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39",
+		},
+		{
+			description:      "Sub Type Negative",
+			givenContextType: 1,
+			givenSubType:     -1,
+			expectedError:    "request.imp[4].native.request.contextsubtype value can't be less than 0. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39",
+		},
+		{
+			description:      "Content - Sub Type Just Below Range",
+			givenContextType: 1, // Content constant
+			givenSubType:     9, // Content range is currently 10-15
+			expectedError:    "request.imp[4].native.request.contextsubtype is invalid. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39",
+		},
+		{
+			description:      "Content - Sub Type In Range",
+			givenContextType: 1,  // Content constant
+			givenSubType:     10, // Content range is currently 10-15
+			expectedError:    "",
+		},
+		{
+			description:      "Content - Sub Type In Range - Context Type Exchange Specific Boundary",
+			givenContextType: 500,
+			givenSubType:     10, // Content range is currently 10-15
+			expectedError:    "",
+		},
+		{
+			description:      "Content - Sub Type In Range - Context Type Exchange Specific Boundary + 1",
+			givenContextType: 501,
+			givenSubType:     10, // Content range is currently 10-15
+			expectedError:    "",
+		},
+		{
+			description:      "Content - Sub Type Just Above Range",
+			givenContextType: 1,  // Content constant
+			givenSubType:     16, // Content range is currently 10-15
+			expectedError:    "request.imp[4].native.request.contextsubtype is invalid. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39",
+		},
+		{
+			description:      "Content - Sub Type Exchange Specific Boundary",
+			givenContextType: 1, // Content constant
+			givenSubType:     500,
+			expectedError:    "",
+		},
+		{
+			description:      "Content - Sub Type Exchange Specific Boundary + 1",
+			givenContextType: 1, // Content constant
+			givenSubType:     501,
+			expectedError:    "",
+		},
+		{
+			description:      "Content - Invalid Context Type",
+			givenContextType: 2,  // Not content constant
+			givenSubType:     10, // Content range is currently 10-15
+			expectedError:    "request.imp[4].native.request.context is 2, but contextsubtype is 10. This is an invalid combination. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39",
+		},
+		{
+			description:      "Social - Sub Type Just Below Range",
+			givenContextType: 2,  // Social constant
+			givenSubType:     19, // Social range is currently 20-22
+			expectedError:    "request.imp[4].native.request.contextsubtype is invalid. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39",
+		},
+		{
+			description:      "Social - Sub Type In Range",
+			givenContextType: 2,  // Social constant
+			givenSubType:     20, // Social range is currently 20-22
+			expectedError:    "",
+		},
+		{
+			description:      "Social - Sub Type In Range - Context Type Exchange Specific Boundary",
+			givenContextType: 500,
+			givenSubType:     20, // Social range is currently 20-22
+			expectedError:    "",
+		},
+		{
+			description:      "Social - Sub Type In Range - Context Type Exchange Specific Boundary + 1",
+			givenContextType: 501,
+			givenSubType:     20, // Social range is currently 20-22
+			expectedError:    "",
+		},
+		{
+			description:      "Social - Sub Type Just Above Range",
+			givenContextType: 2,  // Social constant
+			givenSubType:     23, // Social range is currently 20-22
+			expectedError:    "request.imp[4].native.request.contextsubtype is invalid. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39",
+		},
+		{
+			description:      "Social - Sub Type Exchange Specific Boundary",
+			givenContextType: 2, // Social constant
+			givenSubType:     500,
+			expectedError:    "",
+		},
+		{
+			description:      "Social - Sub Type Exchange Specific Boundary + 1",
+			givenContextType: 2, // Social constant
+			givenSubType:     501,
+			expectedError:    "",
+		},
+		{
+			description:      "Social - Invalid Context Type",
+			givenContextType: 3,  // Not social constant
+			givenSubType:     20, // Social range is currently 20-22
+			expectedError:    "request.imp[4].native.request.context is 3, but contextsubtype is 20. This is an invalid combination. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39",
+		},
+		{
+			description:      "Product - Sub Type Just Below Range",
+			givenContextType: 3,  // Product constant
+			givenSubType:     29, // Product range is currently 30-32
+			expectedError:    "request.imp[4].native.request.contextsubtype is invalid. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39",
+		},
+		{
+			description:      "Product - Sub Type In Range",
+			givenContextType: 3,  // Product constant
+			givenSubType:     30, // Product range is currently 30-32
+			expectedError:    "",
+		},
+		{
+			description:      "Product - Sub Type In Range - Context Type Exchange Specific Boundary",
+			givenContextType: 500,
+			givenSubType:     30, // Product range is currently 30-32
+			expectedError:    "",
+		},
+		{
+			description:      "Product - Sub Type In Range - Context Type Exchange Specific Boundary + 1",
+			givenContextType: 501,
+			givenSubType:     30, // Product range is currently 30-32
+			expectedError:    "",
+		},
+		{
+			description:      "Product - Sub Type Just Above Range",
+			givenContextType: 3,  // Product constant
+			givenSubType:     33, // Product range is currently 30-32
+			expectedError:    "request.imp[4].native.request.contextsubtype is invalid. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39",
+		},
+		{
+			description:      "Product - Sub Type Exchange Specific Boundary",
+			givenContextType: 3, // Product constant
+			givenSubType:     500,
+			expectedError:    "",
+		},
+		{
+			description:      "Product - Sub Type Exchange Specific Boundary + 1",
+			givenContextType: 3, // Product constant
+			givenSubType:     501,
+			expectedError:    "",
+		},
+		{
+			description:      "Product - Invalid Context Type",
+			givenContextType: 1,  // Not product constant
+			givenSubType:     30, // Product range is currently 30-32
+			expectedError:    "request.imp[4].native.request.context is 1, but contextsubtype is 30. This is an invalid combination. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39",
+		},
+	}
+
+	for _, test := range testCases {
+		err := validateNativeContextTypes(test.givenContextType, test.givenSubType, impIndex)
+		if test.expectedError == "" {
+			assert.NoError(t, err, test.description)
+		} else {
+			assert.EqualError(t, err, test.expectedError, test.description)
+		}
+	}
+}
+
+func TestValidateNativePlacementType(t *testing.T) {
+	impIndex := 4
+
+	testCases := []struct {
+		description        string
+		givenPlacementType native1.PlacementType
+		expectedError      string
+	}{
+		{
+			description:        "Not Specified",
+			givenPlacementType: 0,
+			expectedError:      "",
+		},
+		{
+			description:        "Known Value",
+			givenPlacementType: 1, // Range is currently 1-4
+			expectedError:      "",
+		},
+		{
+			description:        "Exchange Specific - Boundary",
+			givenPlacementType: 500,
+			expectedError:      "",
+		},
+		{
+			description:        "Exchange Specific - Boundary + 1",
+			givenPlacementType: 501,
+			expectedError:      "",
+		},
+		{
+			description:        "Negative",
+			givenPlacementType: -1,
+			expectedError:      "request.imp[4].native.request.plcmttype is invalid. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=40",
+		},
+		{
+			description:        "Just Above Range",
+			givenPlacementType: 5, // Range is currently 1-4
+			expectedError:      "request.imp[4].native.request.plcmttype is invalid. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=40",
+		},
+	}
+
+	for _, test := range testCases {
+		err := validateNativePlacementType(test.givenPlacementType, impIndex)
+		if test.expectedError == "" {
+			assert.NoError(t, err, test.description)
+		} else {
+			assert.EqualError(t, err, test.expectedError, test.description)
+		}
+	}
+}
+
+func TestValidateNativeEventTracker(t *testing.T) {
+	impIndex := 4
+	eventIndex := 8
+
+	testCases := []struct {
+		description   string
+		givenEvent    nativeRequests.EventTracker
+		expectedError string
+	}{
+		{
+			description: "Valid",
+			givenEvent: nativeRequests.EventTracker{
+				Event:   1,
+				Methods: []native1.EventTrackingMethod{1},
+			},
+			expectedError: "",
+		},
+		{
+			description: "Event - Exchange Specific - Boundary",
+			givenEvent: nativeRequests.EventTracker{
+				Event:   500,
+				Methods: []native1.EventTrackingMethod{1},
+			},
+			expectedError: "",
+		},
+		{
+			description: "Event - Exchange Specific - Boundary + 1",
+			givenEvent: nativeRequests.EventTracker{
+				Event:   501,
+				Methods: []native1.EventTrackingMethod{1},
+			},
+			expectedError: "",
+		},
+		{
+			description: "Event - Negative",
+			givenEvent: nativeRequests.EventTracker{
+				Event:   -1,
+				Methods: []native1.EventTrackingMethod{1},
+			},
+			expectedError: "request.imp[4].native.request.eventtrackers[8].event is invalid. See section 7.6: https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=43",
+		},
+		{
+			description: "Event - Just Above Range",
+			givenEvent: nativeRequests.EventTracker{
+				Event:   5, // Range is currently 1-4
+				Methods: []native1.EventTrackingMethod{1},
+			},
+			expectedError: "request.imp[4].native.request.eventtrackers[8].event is invalid. See section 7.6: https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=43",
+		},
+		{
+			description: "Methods - Many Valid",
+			givenEvent: nativeRequests.EventTracker{
+				Event:   1,
+				Methods: []native1.EventTrackingMethod{1, 2},
+			},
+			expectedError: "",
+		},
+		{
+			description: "Methods - Empty",
+			givenEvent: nativeRequests.EventTracker{
+				Event:   1,
+				Methods: []native1.EventTrackingMethod{},
+			},
+			expectedError: "request.imp[4].native.request.eventtrackers[8].method is required. See section 7.7: https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=43",
+		},
+		{
+			description: "Methods - Exchange Specific - Boundary",
+			givenEvent: nativeRequests.EventTracker{
+				Event:   1,
+				Methods: []native1.EventTrackingMethod{500},
+			},
+			expectedError: "",
+		},
+		{
+			description: "Methods - Exchange Specific - Boundary + 1",
+			givenEvent: nativeRequests.EventTracker{
+				Event:   1,
+				Methods: []native1.EventTrackingMethod{501},
+			},
+			expectedError: "",
+		},
+		{
+			description: "Methods - Negative",
+			givenEvent: nativeRequests.EventTracker{
+				Event:   1,
+				Methods: []native1.EventTrackingMethod{-1},
+			},
+			expectedError: "request.imp[4].native.request.eventtrackers[8].methods[0] is invalid. See section 7.7: https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=43",
+		},
+		{
+			description: "Methods - Just Above Range",
+			givenEvent: nativeRequests.EventTracker{
+				Event:   1,
+				Methods: []native1.EventTrackingMethod{3}, // Known values are currently 1-2
+			},
+			expectedError: "request.imp[4].native.request.eventtrackers[8].methods[0] is invalid. See section 7.7: https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=43",
+		},
+		{
+			description: "Methods - Mixed Valid + Invalid",
+			givenEvent: nativeRequests.EventTracker{
+				Event:   1,
+				Methods: []native1.EventTrackingMethod{1, -1},
+			},
+			expectedError: "request.imp[4].native.request.eventtrackers[8].methods[1] is invalid. See section 7.7: https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=43",
+		},
+	}
+
+	for _, test := range testCases {
+		err := validateNativeEventTracker(test.givenEvent, impIndex, eventIndex)
+		if test.expectedError == "" {
+			assert.NoError(t, err, test.description)
+		} else {
+			assert.EqualError(t, err, test.expectedError, test.description)
+		}
+	}
+}
+
+func TestValidateNativeAssetData(t *testing.T) {
+	impIndex := 4
+	assetIndex := 8
+
+	testCases := []struct {
+		description   string
+		givenData     nativeRequests.Data
+		expectedError string
+	}{
+		{
+			description:   "Valid",
+			givenData:     nativeRequests.Data{Type: 1},
+			expectedError: "",
+		},
+		{
+			description:   "Exchange Specific - Boundary",
+			givenData:     nativeRequests.Data{Type: 500},
+			expectedError: "",
+		},
+		{
+			description:   "Exchange Specific - Boundary + 1",
+			givenData:     nativeRequests.Data{Type: 501},
+			expectedError: "",
+		},
+		{
+			description:   "Not Specified",
+			givenData:     nativeRequests.Data{},
+			expectedError: "request.imp[4].native.request.assets[8].data.type is invalid. See section 7.4: https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=40",
+		},
+		{
+			description:   "Negative",
+			givenData:     nativeRequests.Data{Type: -1},
+			expectedError: "request.imp[4].native.request.assets[8].data.type is invalid. See section 7.4: https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=40",
+		},
+		{
+			description:   "Just Above Range",
+			givenData:     nativeRequests.Data{Type: 13}, // Range is currently 1-12
+			expectedError: "request.imp[4].native.request.assets[8].data.type is invalid. See section 7.4: https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=40",
+		},
+	}
+
+	for _, test := range testCases {
+		err := validateNativeAssetData(&test.givenData, impIndex, assetIndex)
+		if test.expectedError == "" {
+			assert.NoError(t, err, test.description)
+		} else {
+			assert.EqualError(t, err, test.expectedError, test.description)
+		}
+	}
+}
diff --git a/ortb/request_validator_pmp_test.go b/ortb/request_validator_pmp_test.go
new file mode 100644
index 00000000..99f231f4
--- /dev/null
+++ b/ortb/request_validator_pmp_test.go
@@ -0,0 +1,103 @@
+package ortb
+
+import (
+	"testing"
+
+	"github.com/prebid/openrtb/v20/openrtb2"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestValidatePMP(t *testing.T) {
+	tests := []struct {
+		name      string
+		pmp       *openrtb2.PMP
+		wantError bool
+	}{
+		{
+			name:      "nil",
+			pmp:       nil,
+			wantError: false,
+		},
+		{
+			name: "nil_deals",
+			pmp: &openrtb2.PMP{
+				Deals: nil,
+			},
+			wantError: false,
+		},
+		{
+			name: "empty_deals",
+			pmp: &openrtb2.PMP{
+				Deals: []openrtb2.Deal{},
+			},
+			wantError: false,
+		},
+		{
+			name: "one_deal",
+			pmp: &openrtb2.PMP{
+				Deals: []openrtb2.Deal{
+					{
+						ID: "deal1",
+					},
+				},
+			},
+			wantError: false,
+		},
+		{
+			name: "one_deal_no_id",
+			pmp: &openrtb2.PMP{
+				Deals: []openrtb2.Deal{
+					{
+						ID: "",
+					},
+				},
+			},
+			wantError: true,
+		},
+		{
+			name: "multiple_deals",
+			pmp: &openrtb2.PMP{
+				Deals: []openrtb2.Deal{
+					{
+						ID: "deal1",
+					},
+					{
+						ID: "deal2",
+					},
+					{
+						ID: "deal3",
+					},
+				},
+			},
+			wantError: false,
+		},
+		{
+			name: "multiple_deals_no_id",
+			pmp: &openrtb2.PMP{
+				Deals: []openrtb2.Deal{
+					{
+						ID: "deal1",
+					},
+					{
+						ID: "",
+					},
+					{
+						ID: "deal3",
+					},
+				},
+			},
+			wantError: true,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			result := validatePmp(test.pmp, 1)
+			if test.wantError {
+				assert.Error(t, result)
+			} else {
+				assert.NoError(t, result)
+			}
+		})
+	}
+}
diff --git a/ortb/request_validator_test.go b/ortb/request_validator_test.go
new file mode 100644
index 00000000..1c84a6ca
--- /dev/null
+++ b/ortb/request_validator_test.go
@@ -0,0 +1,237 @@
+package ortb
+
+import (
+	"encoding/json"
+	"errors"
+	"testing"
+
+	"github.com/prebid/openrtb/v20/openrtb2"
+	"github.com/prebid/prebid-server/v2/errortypes"
+	"github.com/prebid/prebid-server/v2/openrtb_ext"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestValidateImpExt(t *testing.T) {
+	type testCase struct {
+		description    string
+		impExt         json.RawMessage
+		expectedImpExt string
+		expectedErrs   []error
+	}
+	testGroups := []struct {
+		description string
+		testCases   []testCase
+	}{
+		{
+			"Empty",
+			[]testCase{
+				{
+					description:    "Empty",
+					impExt:         nil,
+					expectedImpExt: "",
+					expectedErrs:   []error{errors.New("request.imp[0].ext is required")},
+				},
+			},
+		},
+		{
+			"Unknown bidder tests",
+			[]testCase{
+				{
+					description:    "Unknown Bidder only",
+					impExt:         json.RawMessage(`{"unknownbidder":{"placement_id":555}}`),
+					expectedImpExt: `{"unknownbidder":{"placement_id":555}}`,
+					expectedErrs:   []error{errors.New("request.imp[0].ext.prebid.bidder contains unknown bidder: unknownbidder. Did you forget an alias in request.ext.prebid.aliases?")},
+				},
+				{
+					description:    "Unknown Prebid Ext Bidder only",
+					impExt:         json.RawMessage(`{"prebid":{"bidder":{"unknownbidder":{"placement_id":555}}}}`),
+					expectedImpExt: `{"prebid":{"bidder":{"unknownbidder":{"placement_id":555}}}}`,
+					expectedErrs:   []error{errors.New("request.imp[0].ext.prebid.bidder contains unknown bidder: unknownbidder. Did you forget an alias in request.ext.prebid.aliases?")},
+				},
+				{
+					description:    "Unknown Prebid Ext Bidder + First Party Data Context",
+					impExt:         json.RawMessage(`{"prebid":{"bidder":{"unknownbidder":{"placement_id":555}}},"context":{"data":{"keywords":"prebid server example"}}}`),
+					expectedImpExt: `{"prebid":{"bidder":{"unknownbidder":{"placement_id":555}}},"context":{"data":{"keywords":"prebid server example"}}}`,
+					expectedErrs:   []error{errors.New("request.imp[0].ext.prebid.bidder contains unknown bidder: unknownbidder. Did you forget an alias in request.ext.prebid.aliases?")},
+				},
+				{
+					description:    "Unknown Bidder + First Party Data Context",
+					impExt:         json.RawMessage(`{"unknownbidder":{"placement_id":555} ,"context":{"data":{"keywords":"prebid server example"}}}`),
+					expectedImpExt: `{"unknownbidder":{"placement_id":555},"context":{"data":{"keywords":"prebid server example"}}}`,
+					expectedErrs:   []error{errors.New("request.imp[0].ext.prebid.bidder contains unknown bidder: unknownbidder. Did you forget an alias in request.ext.prebid.aliases?")},
+				},
+				{
+					description:    "Unknown Bidder + Disabled Bidder",
+					impExt:         json.RawMessage(`{"unknownbidder":{"placement_id":555},"disabledbidder":{"foo":"bar"}}`),
+					expectedImpExt: `{"unknownbidder":{"placement_id":555},"disabledbidder":{"foo":"bar"}}`,
+					expectedErrs:   []error{errors.New("request.imp[0].ext.prebid.bidder contains unknown bidder: unknownbidder. Did you forget an alias in request.ext.prebid.aliases?")},
+				},
+				{
+					description:    "Unknown Bidder + Disabled Prebid Ext Bidder",
+					impExt:         json.RawMessage(`{"unknownbidder":{"placement_id":555},"prebid":{"bidder":{"disabledbidder":{"foo":"bar"}}}}`),
+					expectedImpExt: `{"unknownbidder":{"placement_id":555},"prebid":{"bidder":{"disabledbidder":{"foo":"bar"}}}}`,
+					expectedErrs:   []error{errors.New("request.imp[0].ext.prebid.bidder contains unknown bidder: unknownbidder. Did you forget an alias in request.ext.prebid.aliases?")},
+				},
+			},
+		},
+		{
+			"Disabled bidder tests",
+			[]testCase{
+				{
+					description:    "Disabled Bidder",
+					impExt:         json.RawMessage(`{"disabledbidder":{"foo":"bar"}}`),
+					expectedImpExt: `{"disabledbidder":{"foo":"bar"}}`,
+					expectedErrs: []error{
+						&errortypes.BidderTemporarilyDisabled{Message: "The bidder 'disabledbidder' has been disabled."},
+						errors.New("request.imp[0].ext.prebid.bidder must contain at least one bidder"),
+					},
+					// if only bidder(s) found in request.imp[x].ext.{biddername} or request.imp[x].ext.prebid.bidder.{biddername} are disabled, return error
+				},
+				{
+					description:    "Disabled Prebid Ext Bidder",
+					impExt:         json.RawMessage(`{"prebid":{"bidder":{"disabledbidder":{"foo":"bar"}}}}`),
+					expectedImpExt: `{"prebid":{"bidder":{"disabledbidder":{"foo":"bar"}}}}`,
+					expectedErrs: []error{
+						&errortypes.BidderTemporarilyDisabled{Message: "The bidder 'disabledbidder' has been disabled."},
+						errors.New("request.imp[0].ext.prebid.bidder must contain at least one bidder"),
+					},
+				},
+				{
+					description:    "Disabled Bidder + First Party Data Context",
+					impExt:         json.RawMessage(`{"disabledbidder":{"foo":"bar"},"context":{"data":{"keywords":"prebid server example"}}}`),
+					expectedImpExt: `{"disabledbidder":{"foo":"bar"},"context":{"data":{"keywords":"prebid server example"}}}`,
+					expectedErrs: []error{
+						&errortypes.BidderTemporarilyDisabled{Message: "The bidder 'disabledbidder' has been disabled."},
+						errors.New("request.imp[0].ext.prebid.bidder must contain at least one bidder"),
+					},
+				},
+				{
+					description:    "Disabled Prebid Ext Bidder + First Party Data Context",
+					impExt:         json.RawMessage(`{"prebid":{"bidder":{"disabledbidder":{"foo":"bar"}}},"context":{"data":{"keywords":"prebid server example"}}}`),
+					expectedImpExt: `{"prebid":{"bidder":{"disabledbidder":{"foo":"bar"}}},"context":{"data":{"keywords":"prebid server example"}}}`,
+					expectedErrs: []error{
+						&errortypes.BidderTemporarilyDisabled{Message: "The bidder 'disabledbidder' has been disabled."},
+						errors.New("request.imp[0].ext.prebid.bidder must contain at least one bidder"),
+					},
+				},
+			},
+		},
+		{
+			"First Party only",
+			[]testCase{
+				{
+					description:    "First Party Data Context",
+					impExt:         json.RawMessage(`{"context":{"data":{"keywords":"prebid server example"}}}`),
+					expectedImpExt: `{"context":{"data":{"keywords":"prebid server example"}}}`,
+					expectedErrs: []error{
+						errors.New("request.imp[0].ext.prebid.bidder must contain at least one bidder"),
+					},
+				},
+			},
+		},
+		{
+			"Valid bidder tests",
+			[]testCase{
+				{
+					description:    "Valid bidder root ext",
+					impExt:         json.RawMessage(`{"appnexus":{"placement_id":555}}`),
+					expectedImpExt: `{"prebid":{"bidder":{"appnexus":{"placement_id":555}}}}`,
+					expectedErrs:   []error{},
+				},
+				{
+					description:    "Valid bidder in prebid field",
+					impExt:         json.RawMessage(`{"prebid":{"bidder":{"appnexus":{"placement_id":555}}}}`),
+					expectedImpExt: `{"prebid":{"bidder":{"appnexus":{"placement_id":555}}}}`,
+					expectedErrs:   []error{},
+				},
+				{
+					description:    "Valid Bidder + First Party Data Context",
+					impExt:         json.RawMessage(`{"appnexus":{"placement_id":555},"context":{"data":{"keywords":"prebid server example"}}}`),
+					expectedImpExt: `{"prebid":{"bidder":{"appnexus":{"placement_id":555}}},"context":{"data":{"keywords":"prebid server example"}}}`,
+					expectedErrs:   []error{},
+				},
+				{
+					description:    "Valid Prebid Ext Bidder + First Party Data Context",
+					impExt:         json.RawMessage(`{"prebid":{"bidder":{"appnexus":{"placement_id":555}}} ,"context":{"data":{"keywords":"prebid server example"}}}`),
+					expectedImpExt: `{"prebid":{"bidder":{"appnexus":{"placement_id":555}}},"context":{"data":{"keywords":"prebid server example"}}}`,
+					expectedErrs:   []error{},
+				},
+				{
+					description:    "Valid Bidder + Unknown Bidder",
+					impExt:         json.RawMessage(`{"appnexus":{"placement_id":555},"unknownbidder":{"placement_id":555}}`),
+					expectedImpExt: `{"appnexus":{"placement_id":555},"unknownbidder":{"placement_id":555}}`,
+					expectedErrs:   []error{errors.New("request.imp[0].ext.prebid.bidder contains unknown bidder: unknownbidder. Did you forget an alias in request.ext.prebid.aliases?")},
+				},
+				{
+					description:    "Valid Bidder + Disabled Bidder",
+					impExt:         json.RawMessage(`{"appnexus":{"placement_id":555},"disabledbidder":{"foo":"bar"}}`),
+					expectedImpExt: `{"prebid":{"bidder":{"appnexus":{"placement_id":555}}}}`,
+					expectedErrs:   []error{&errortypes.BidderTemporarilyDisabled{Message: "The bidder 'disabledbidder' has been disabled."}},
+				},
+				{
+					description:    "Valid Bidder + Disabled Bidder + First Party Data Context",
+					impExt:         json.RawMessage(`{"appnexus":{"placement_id":555},"disabledbidder":{"foo":"bar"},"context":{"data":{"keywords":"prebid server example"}}}`),
+					expectedImpExt: `{"prebid":{"bidder":{"appnexus":{"placement_id":555}}},"context":{"data":{"keywords":"prebid server example"}}}`,
+					expectedErrs:   []error{&errortypes.BidderTemporarilyDisabled{Message: "The bidder 'disabledbidder' has been disabled."}},
+				},
+				{
+					description:    "Valid Bidder + Disabled Bidder + Unknown Bidder + First Party Data Context",
+					impExt:         json.RawMessage(`{"appnexus":{"placement_id":555},"disabledbidder":{"foo":"bar"},"unknownbidder":{"placement_id":555},"context":{"data":{"keywords":"prebid server example"}}}`),
+					expectedImpExt: `{"appnexus":{"placement_id":555},"disabledbidder":{"foo":"bar"},"unknownbidder":{"placement_id":555},"context":{"data":{"keywords":"prebid server example"}}}`,
+					expectedErrs:   []error{errors.New("request.imp[0].ext.prebid.bidder contains unknown bidder: unknownbidder. Did you forget an alias in request.ext.prebid.aliases?")},
+				},
+				{
+					description:    "Valid Prebid Ext Bidder + Disabled Bidder Ext",
+					impExt:         json.RawMessage(`{"prebid":{"bidder":{"appnexus":{"placement_id":555},"disabledbidder":{"foo":"bar"}}}}`),
+					expectedImpExt: `{"prebid":{"bidder":{"appnexus":{"placement_id": 555}}}}`,
+					expectedErrs:   []error{&errortypes.BidderTemporarilyDisabled{Message: "The bidder 'disabledbidder' has been disabled."}},
+				},
+				{
+					description:    "Valid Prebid Ext Bidder + Disabled Ext Bidder + First Party Data Context",
+					impExt:         json.RawMessage(`{"prebid":{"bidder":{"appnexus":{"placement_id":555},"disabledbidder":{"foo":"bar"}}},"context":{"data":{"keywords":"prebid server example"}}}`),
+					expectedImpExt: `{"prebid":{"bidder":{"appnexus":{"placement_id": 555}}},"context":{"data":{"keywords":"prebid server example"}}}`,
+					expectedErrs:   []error{&errortypes.BidderTemporarilyDisabled{Message: "The bidder 'disabledbidder' has been disabled."}},
+				},
+				{
+					description:    "Valid Prebid Ext Bidder + Disabled Ext Bidder + Unknown Ext + First Party Data Context",
+					impExt:         json.RawMessage(`{"prebid":{"bidder":{"appnexus":{"placement_id":555},"disabledbidder":{"foo":"bar"},"unknownbidder":{"placement_id":555}}},"context":{"data":{"keywords":"prebid server example"}}}`),
+					expectedImpExt: `{"prebid":{"bidder":{"appnexus":{"placement_id":555},"disabledbidder":{"foo":"bar"},"unknownbidder":{"placement_id":555}}},"context":{"data":{"keywords":"prebid server example"}}}`,
+					expectedErrs:   []error{errors.New("request.imp[0].ext.prebid.bidder contains unknown bidder: unknownbidder. Did you forget an alias in request.ext.prebid.aliases?")},
+				},
+			},
+		},
+	}
+
+	for _, group := range testGroups {
+		for _, test := range group.testCases {
+			t.Run(test.description, func(t *testing.T) {
+				imp := &openrtb2.Imp{Ext: test.impExt}
+				impWrapper := &openrtb_ext.ImpWrapper{Imp: imp}
+
+				disabledBidders := map[string]string{"disabledbidder": "The bidder 'disabledbidder' has been disabled."}
+				rv := standardRequestValidator{
+					bidderMap:       openrtb_ext.BuildBidderMap(),
+					disabledBidders: disabledBidders,
+					paramsValidator: mockBidderParamValidator{},
+				}
+				errs := rv.validateImpExt(impWrapper, nil, 0, false, nil)
+
+				assert.NoError(t, impWrapper.RebuildImp(), test.description+":rebuild_imp")
+
+				if len(test.expectedImpExt) > 0 {
+					assert.JSONEq(t, test.expectedImpExt, string(imp.Ext), "imp.ext JSON does not match expected. Test: %s. %s\n", group.description, test.description)
+				} else {
+					assert.Empty(t, imp.Ext, "imp.ext expected to be empty but was: %s. Test: %s. %s\n", string(imp.Ext), group.description, test.description)
+				}
+				assert.Equal(t, test.expectedErrs, errs, "errs slice does not match expected. Test: %s. %s\n", group.description, test.description)
+			})
+		}
+	}
+}
+
+type mockBidderParamValidator struct{}
+
+func (v mockBidderParamValidator) Validate(name openrtb_ext.BidderName, ext json.RawMessage) error {
+	return nil
+}
+func (v mockBidderParamValidator) Schema(name openrtb_ext.BidderName) string { return "" }
diff --git a/ortb/request_validator_video_test.go b/ortb/request_validator_video_test.go
new file mode 100644
index 00000000..10dbfee4
--- /dev/null
+++ b/ortb/request_validator_video_test.go
@@ -0,0 +1,111 @@
+package ortb
+
+import (
+	"testing"
+
+	"github.com/prebid/openrtb/v20/openrtb2"
+	"github.com/prebid/prebid-server/v2/util/ptrutil"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestValidateVideo(t *testing.T) {
+	tests := []struct {
+		name      string
+		video     *openrtb2.Video
+		wantError bool
+	}{
+		{
+			name:      "nil",
+			video:     nil,
+			wantError: false,
+		},
+		{
+			name: "well_formed",
+			video: &openrtb2.Video{
+				MIMEs:      []string{"MIME1"},
+				W:          ptrutil.ToPtr[int64](0),
+				H:          ptrutil.ToPtr[int64](0),
+				MinBitRate: 0,
+				MaxBitRate: 0,
+			},
+			wantError: false,
+		},
+		{
+			name: "well_formed_with_nil_dims",
+			video: &openrtb2.Video{
+				MIMEs:      []string{"MIME1"},
+				W:          nil,
+				H:          nil,
+				MinBitRate: 0,
+				MaxBitRate: 0,
+			},
+			wantError: false,
+		},
+		{
+			name: "mimes_is_zero",
+			video: &openrtb2.Video{
+				MIMEs:      []string{},
+				W:          ptrutil.ToPtr[int64](0),
+				H:          ptrutil.ToPtr[int64](0),
+				MinBitRate: 0,
+				MaxBitRate: 0,
+			},
+			wantError: true,
+		},
+		{
+			name: "negative_width",
+			video: &openrtb2.Video{
+				MIMEs:      []string{"MIME1"},
+				W:          ptrutil.ToPtr[int64](-1),
+				H:          ptrutil.ToPtr[int64](0),
+				MinBitRate: 0,
+				MaxBitRate: 0,
+			},
+			wantError: true,
+		},
+		{
+			name: "negative_height",
+			video: &openrtb2.Video{
+				MIMEs:      []string{"MIME1"},
+				W:          ptrutil.ToPtr[int64](0),
+				H:          ptrutil.ToPtr[int64](-1),
+				MinBitRate: 0,
+				MaxBitRate: 0,
+			},
+			wantError: true,
+		},
+		{
+			name: "negative_min_bit_rate",
+			video: &openrtb2.Video{
+				MIMEs:      []string{"MIME1"},
+				W:          ptrutil.ToPtr[int64](0),
+				H:          ptrutil.ToPtr[int64](0),
+				MinBitRate: -1,
+				MaxBitRate: 0,
+			},
+			wantError: true,
+		},
+		{
+			name: "negative_max_bit_rate",
+			video: &openrtb2.Video{
+				MIMEs:      []string{"MIME1"},
+				W:          ptrutil.ToPtr[int64](0),
+				H:          ptrutil.ToPtr[int64](0),
+				MinBitRate: 0,
+				MaxBitRate: -1,
+			},
+			wantError: true,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			result := validateVideo(test.video, 1)
+			if test.wantError {
+				assert.Error(t, result)
+			} else {
+				assert.NoError(t, result)
+			}
+		})
+	}
+}
diff --git a/util/jsonutil/merge_test.go b/util/jsonutil/merge_test.go
index 8c85d623..217a980e 100644
--- a/util/jsonutil/merge_test.go
+++ b/util/jsonutil/merge_test.go
@@ -139,6 +139,22 @@ func TestMergeCloneSlice(t *testing.T) {
 		assert.Equal(t, []string{"b", "c"}, imp.IframeBuster, "new-val")
 	})
 
+	t.Run("replace-not-overlay", func(t *testing.T) {
+		var (
+			imp      = &openrtb2.Imp{ID: "1"}
+			impSlice = []*openrtb2.Imp{imp}
+			test     = &struct {
+				Imps []*openrtb2.Imp `json:"imps"`
+			}{Imps: impSlice}
+		)
+
+		err := MergeClone(test, []byte(`{"imps":[{"tagid":"2"}]}`))
+		require.NoError(t, err)
+
+		impExpected := &openrtb2.Imp{TagID: "2"} // ensure original id is no longer present
+		assert.Equal(t, []*openrtb2.Imp{impExpected}, test.Imps)
+	})
+
 	t.Run("invalid-null", func(t *testing.T) {
 		var (
 			iframeBuster = []string{"a"}
diff --git a/util/reflectutil/slice_test.go b/util/reflectutil/slice_test.go
deleted file mode 100644
index d5183892..00000000
--- a/util/reflectutil/slice_test.go
+++ /dev/null
@@ -1,78 +0,0 @@
-package reflectutil
-
-import (
-	"testing"
-
-	"github.com/modern-go/reflect2"
-	"github.com/stretchr/testify/assert"
-)
-
-func TestUnsafeSliceCloneSimple(t *testing.T) {
-	testCases := []struct {
-		name  string
-		given []int
-	}{
-		{
-			name:  "empty",
-			given: []int{},
-		},
-		{
-			name:  "one",
-			given: []int{1},
-		},
-		{
-			name:  "many",
-			given: []int{1, 2},
-		},
-	}
-
-	for _, test := range testCases {
-		t.Run(test.name, func(t *testing.T) {
-			original := test.given
-			clonePtr := UnsafeSliceClone(reflect2.PtrOf(test.given), reflect2.TypeOf([]int{}).(*reflect2.UnsafeSliceType))
-			clone := *(*[]int)(clonePtr)
-
-			assert.NotSame(t, original, clone, "reference")
-			assert.Equal(t, original, clone, "equality")
-			assert.Equal(t, len(original), len(clone), "len")
-			assert.Equal(t, cap(original), cap(clone), "cap")
-		})
-	}
-}
-
-func TestUnsafeSliceCloneComplex(t *testing.T) {
-	type foo struct {
-		value string
-	}
-
-	testCases := []struct {
-		name  string
-		given []foo
-	}{
-		{
-			name:  "empty",
-			given: []foo{},
-		},
-		{
-			name:  "one",
-			given: []foo{{value: "a"}},
-		},
-		{
-			name:  "many",
-			given: []foo{{value: "a"}, {value: "b"}},
-		},
-	}
-
-	for _, test := range testCases {
-		t.Run(test.name, func(t *testing.T) {
-			original := test.given
-			clonePtr := UnsafeSliceClone(reflect2.PtrOf(test.given), reflect2.TypeOf([]foo{}).(*reflect2.UnsafeSliceType))
-			clone := *(*[]foo)(clonePtr)
-
-			assert.NotSame(t, original, clone, "reference")
-			assert.Equal(t, original, clone, "equality")
-			assert.Equal(t, len(original), len(clone), "len")
-			assert.Equal(t, cap(original), cap(clone), "cap")
-		})
-	}
-}