Repository: prebid/prebid-server
Pull request #3697 (2024-05-21 17:56:59)

Yieldmo: Add bid floor currency conversion
This change adds bid floor currency conversion for the Yieldmo adapter. I didn't see any documentation around this functionality, so I largely based this change on the implementations I saw across other adapters. If anyone reviewing this is aware of any documentation or known issues/edge cases with this feature, I would love to hear about them. Thanks!

github-actions[bot]: ## Code coverage summary 
Note: 
- Prebid team doesn't anticipate tests covering code paths that might result in marshal and unmarshal errors 
- Coverage summary encompasses all commits leading up to the latest one, 8837dd193093b44929a7b7ecc8b8180cc8ffa2b0 
#### yieldmo 
Refer [here](https://htmlpreview.github.io/?https://github.com/prebid/prebid-server/coverage-preview/.github/preview/9179181168_1716314264/yieldmo.html) for heat map coverage report 
``` 
github.com/prebid/prebid-server/v2/adapters/yieldmo/yieldmo.go:38:	MakeRequests		70.0%
github.com/prebid/prebid-server/v2/adapters/yieldmo/yieldmo.go:65:	preprocess		66.7%
github.com/prebid/prebid-server/v2/adapters/yieldmo/yieldmo.go:118:	MakeBids		70.6%
github.com/prebid/prebid-server/v2/adapters/yieldmo/yieldmo.go:160:	Builder			100.0%
github.com/prebid/prebid-server/v2/adapters/yieldmo/yieldmo.go:168:	getMediaTypeForImp	62.5%
total:									(statements)		68.9%

 ``` 

ym-winston: I'm not sure why the validate merge failed. When I run ./validate.sh --nofmt --cov locally I see a handful of warnings but no errors or failures. Please let me know if you have any advice on what to do about that. 

Patch:
diff --git a/README.md b/README.md
index c5b0e003..20cccf8a 100644
--- a/README.md
+++ b/README.md
@@ -105,3 +105,9 @@ We welcome everyone to contribute to this project by implementing a specificatio
 
 ### Bug Fix
 Bug reports may be submitted by [opening a new issue](https://github.com/prebid/prebid-server/issues/new/choose) and describing the error in detail with the steps to reproduce and example data. A member of the core development team will validate the bug and discuss next steps. You're encouraged to open an exploratory draft pull request to either demonstrate the bug by adding a test or offering a potential fix.
+The quickest way to start developing Prebid Server in a reproducible environment isolated from your host OS
+is by using Visual Studio Code with [Remote Container Setup](devcontainer.md).
+
+## Learning Materials
+
+To understand more about how Prebid Server in Go works and quickly spins up sample instances, refer to the `sample` folder which describes various structured and integrated examples. The examples are designed to run on any platform that supports `docker` container.
diff --git a/adapters/medianet/medianet.go b/adapters/medianet/medianet.go
index f10ca986..a6a3bd47 100644
--- a/adapters/medianet/medianet.go
+++ b/adapters/medianet/medianet.go
@@ -67,7 +67,7 @@ func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest
 
 	for _, sb := range bidResp.SeatBid {
 		for i := range sb.Bid {
-			bidType, err := getMediaTypeForImp(sb.Bid[i].ImpID, internalRequest.Imp)
+			bidType, err := getBidMediaTypeFromMtype(&sb.Bid[i])
 			if err != nil {
 				errs = append(errs, err)
 			} else {
@@ -90,19 +90,16 @@ func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server co
 	}, nil
 }
 
-func getMediaTypeForImp(impID string, imps []openrtb2.Imp) (openrtb_ext.BidType, error) {
-	mediaType := openrtb_ext.BidTypeBanner
-	for _, imp := range imps {
-		if imp.ID == impID {
-			if imp.Banner == nil && imp.Video != nil {
-				mediaType = openrtb_ext.BidTypeVideo
-			}
-			return mediaType, nil
-		}
-	}
-
-	return "", &errortypes.BadInput{
-		Message: fmt.Sprintf("Failed to find impression \"%s\" ", impID),
+func getBidMediaTypeFromMtype(bid *openrtb2.Bid) (openrtb_ext.BidType, error) {
+	switch bid.MType {
+	case openrtb2.MarkupBanner:
+		return openrtb_ext.BidTypeBanner, nil
+	case openrtb2.MarkupVideo:
+		return openrtb_ext.BidTypeVideo, nil
+	case openrtb2.MarkupNative:
+		return openrtb_ext.BidTypeNative, nil
+	default:
+		return "", fmt.Errorf("Unable to fetch mediaType for imp: %s", bid.ImpID)
 	}
 }
 
diff --git a/adapters/pubmatic/pubmatic.go b/adapters/pubmatic/pubmatic.go
index da6092d8..9c5799ee 100644
--- a/adapters/pubmatic/pubmatic.go
+++ b/adapters/pubmatic/pubmatic.go
@@ -48,6 +48,7 @@ type ExtImpBidderPubmatic struct {
 	adapters.ExtImpBidder
 	Data json.RawMessage `json:"data,omitempty"`
 	AE   int             `json:"ae,omitempty"`
+	GpId string          `json:"gpid,omitempty"`
 }
 
 type ExtAdServer struct {
@@ -78,6 +79,7 @@ const (
 	AdServerGAM        = "gam"
 	AdServerKey        = "adserver"
 	PBAdslotKey        = "pbadslot"
+	gpIdKey            = "gpid"
 )
 
 func (a *PubmaticAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
@@ -319,6 +321,10 @@ func parseImpressionObject(imp *openrtb2.Imp, extractWrapperExtFromImp, extractP
 		extMap[ae] = bidderExt.AE
 	}
 
+	if bidderExt.GpId != "" {
+		extMap[gpIdKey] = bidderExt.GpId
+	}
+
 	imp.Ext = nil
 	if len(extMap) > 0 {
 		ext, err := json.Marshal(extMap)
diff --git a/adapters/rubicon/rubicon.go b/adapters/rubicon/rubicon.go
index a12d2fc7..6e515b11 100644
--- a/adapters/rubicon/rubicon.go
+++ b/adapters/rubicon/rubicon.go
@@ -331,14 +331,9 @@ func (a *RubiconAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *ada
 			}
 
 			userExtRP := rubiconUserExt{RP: rubiconUserExtRP{Target: target}}
-			userBuyerUID := userCopy.BuyerUID
 
 			if len(userCopy.EIDs) > 0 {
 				userExtRP.Eids = userCopy.EIDs
-
-				if userBuyerUID == "" {
-					userBuyerUID = extractUserBuyerUID(userExtRP.Eids)
-				}
 			}
 
 			if userCopy.Consent != "" {
@@ -354,7 +349,6 @@ func (a *RubiconAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *ada
 			userCopy.Geo = nil
 			userCopy.Yob = 0
 			userCopy.Gender = ""
-			userCopy.BuyerUID = userBuyerUID
 			userCopy.EIDs = nil
 
 			rubiconRequest.User = &userCopy
@@ -914,20 +908,6 @@ func contains(s []int, e int) bool {
 	return false
 }
 
-func extractUserBuyerUID(eids []openrtb2.EID) string {
-	for _, eid := range eids {
-		if eid.Source != "rubiconproject.com" {
-			continue
-		}
-
-		for _, uid := range eid.UIDs {
-			return uid.ID
-		}
-	}
-
-	return ""
-}
-
 func isVideo(imp openrtb2.Imp) bool {
 	video := imp.Video
 	if video != nil {
diff --git a/adapters/unruly/unruly.go b/adapters/unruly/unruly.go
index cdea14d3..97fc2b64 100644
--- a/adapters/unruly/unruly.go
+++ b/adapters/unruly/unruly.go
@@ -113,7 +113,7 @@ func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest
 		}}
 	}
 
-	bidResponse := adapters.NewBidderResponseWithBidsCapacity(5)
+	bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(internalRequest.Imp))
 
 	var errs []error
 	for _, sb := range bidResp.SeatBid {
diff --git a/adapters/yeahmobi/yeahmobi.go b/adapters/yeahmobi/yeahmobi.go
index 6d351d85..b533e76b 100644
--- a/adapters/yeahmobi/yeahmobi.go
+++ b/adapters/yeahmobi/yeahmobi.go
@@ -18,6 +18,12 @@ import (
 type adapter struct {
 	EndpointTemplate *template.Template
 }
+type yeahmobiBidExt struct {
+	VideoCreativeInfo *yeahmobiBidExtVideo `json:"video,omitempty"`
+}
+type yeahmobiBidExtVideo struct {
+	Duration *int `json:"duration,omitempty"`
+}
 
 // Builder builds a new instance of the Yeahmobi adapter for the given bidder with the given config.
 func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
@@ -134,34 +140,42 @@ func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest
 	if response.StatusCode == http.StatusNoContent {
 		return nil, nil
 	}
-
 	if response.StatusCode == http.StatusBadRequest {
 		return nil, []error{&errortypes.BadInput{
 			Message: fmt.Sprintf("Unexpected status code: %d.", response.StatusCode),
 		}}
 	}
-
 	if response.StatusCode != http.StatusOK {
 		return nil, []error{&errortypes.BadServerResponse{
 			Message: fmt.Sprintf("Unexpected status code: %d.", response.StatusCode),
 		}}
 	}
-
 	var bidResp openrtb2.BidResponse
-
 	if err := json.Unmarshal(response.Body, &bidResp); err != nil {
 		return nil, []error{err}
 	}
-
 	bidResponse := adapters.NewBidderResponseWithBidsCapacity(1)
-
 	for _, sb := range bidResp.SeatBid {
 		for i := range sb.Bid {
 			var mediaType = getBidType(sb.Bid[i].ImpID, internalRequest.Imp)
-			bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
-				Bid:     &sb.Bid[i],
-				BidType: mediaType,
-			})
+			bid := sb.Bid[i]
+			typedBid := &adapters.TypedBid{
+				Bid:      &bid,
+				BidType:  mediaType,
+				BidVideo: &openrtb_ext.ExtBidPrebidVideo{},
+			}
+			if bid.Ext != nil {
+				var bidExt *yeahmobiBidExt
+				err := json.Unmarshal(bid.Ext, &bidExt)
+				if err != nil {
+					return nil, []error{fmt.Errorf("bid.ext json unmarshal error")}
+				} else if bidExt != nil {
+					if bidExt.VideoCreativeInfo != nil && bidExt.VideoCreativeInfo.Duration != nil {
+						typedBid.BidVideo.Duration = *bidExt.VideoCreativeInfo.Duration
+					}
+				}
+			}
+			bidResponse.Bids = append(bidResponse.Bids, typedBid)
 		}
 	}
 	return bidResponse, nil
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..4dbb0301 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,
diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go
index a1d4a98f..eedb1b71 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) {
@@ -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
 		}
 
@@ -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, hasStoredResponses, 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/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/ortb/request_validator.go b/ortb/request_validator.go
new file mode 100644
index 00000000..79d1f930
--- /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, hasStoredResponses 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, hasStoredResponses 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, hasStoredResponses, storedBidResponses)
+	if len(errL) != 0 {
+		return errL
+	}
+
+	return nil
+}
+
+func (srv *standardRequestValidator) 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 openrtb_ext.IsPotentialBidder(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 := 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..308f8d4f 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,6 +236,7 @@ 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)
@@ -242,17 +244,17 @@ func New(cfg *config.Configuration, rateConvertor *currency.RateConverter) (r *R
 	macroReplacer := macros.NewStringIndexBasedReplacer()
 	theExchange := exchange.NewExchange(adapters, cacheClient, cfg, 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/sample/001_banner/app.yaml b/sample/001_banner/app.yaml
new file mode 100644
index 00000000..a314e77e
--- /dev/null
+++ b/sample/001_banner/app.yaml
@@ -0,0 +1,20 @@
+port: 8000 # main auction server port
+admin_port: 6060 # admin server listening port
+
+external_url: localhost # host url of all 2 servers above
+status_response: "ok" # default response string for /status endpoint
+
+gdpr:
+  default_value: "0" # disable gdpr, explicitly specifying a default value is a requirement in prebid server config
+
+# set up stored request storage using local file system
+stored_requests:
+  filesystem: 
+    enabled: true
+    directorypath: ./stored_requests/data/by_id
+
+# set up stored response storage using local file system
+stored_responses:
+  filesystem:
+    enabled: true
+    directorypath: ./stored_responses/data/by_id
\ No newline at end of file
diff --git a/sample/001_banner/pbjs.html b/sample/001_banner/pbjs.html
new file mode 100644
index 00000000..b02ba95b
--- /dev/null
+++ b/sample/001_banner/pbjs.html
@@ -0,0 +1,121 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <style>
+        div.adslot {
+            border-color: red;
+            border-style: solid;
+            width: 300px;
+            height: 600px;
+        }
+        p.description {
+            border-style: solid;
+        }
+    </style>
+    <script src="//cdn.jsdelivr.net/npm/prebid.js@latest/dist/not-for-prod/prebid.js"></script>
+    <script>
+
+        var sizes = [
+            [300, 250],
+            [300, 600]
+        ];
+
+        // use the cool "Prebid Less" feature https://docs.prebid.org/dev-docs/adunit-reference.html#stored-imp
+        var adUnits = [
+            {
+                code: 'test-div-1',
+                mediaTypes: {
+                    banner: {
+                        sizes: sizes
+                    }
+                },
+	        bids: [{
+	            module: "pbsBidAdapter",
+	            ortb2Imp: {
+		      ext: { prebid: { storedrequest: { id: 'test-imp-id' }}}
+                    }
+                }]
+            }
+        ]
+
+        function renderAllAdUnits() {
+            var winners = pbjs.getHighestCpmBids();
+            for (var i = 0; i < winners.length; i++) {
+                renderOne(winners[i]);
+            }
+        }
+
+        // create an iframe in the div and write the winning ad into it
+        function renderOne(winningBid) {
+            if (winningBid && winningBid.adId) {
+                var div = document.getElementById(winningBid.adUnitCode);
+                if (div) {
+                    const iframe = document.createElement('iframe');
+                    iframe.scrolling = 'no';
+                    iframe.frameBorder = '0';
+                    iframe.marginHeight = '0';
+                    iframe.marginHeight = '0';
+                    iframe.name = `prebid_ads_iframe_${winningBid.adUnitCode}`;
+                    iframe.title = '3rd party ad content';
+                    iframe.sandbox.add(
+                        'allow-forms',
+                        'allow-popups',
+                        'allow-popups-to-escape-sandbox',
+                        'allow-same-origin',
+                        'allow-scripts',
+                        'allow-top-navigation-by-user-activation'
+                    );
+                    iframe.setAttribute('aria-label', 'Advertisment');
+                    iframe.style.setProperty('border', '0');
+                    iframe.style.setProperty('margin', '0');
+                    iframe.style.setProperty('overflow', 'hidden');
+                    div.appendChild(iframe);
+                    const iframeDoc = iframe.contentWindow.document;
+                    pbjs.renderAd(iframeDoc, winningBid.adId);
+                }
+            }
+        }
+
+    </script>
+    <script>
+        var pbjs = pbjs || {};
+        pbjs.que = pbjs.que || [];
+
+        pbjs.que.push(function () {
+            pbjs.setConfig({
+                s2sConfig: [{
+                    accountId: '1',
+                    adapter: "prebidServer",
+                    enabled: true,
+                    endpoint: {
+                        noP1Consent: "http://localhost:8000/openrtb2/auction",
+                        p1Consent: 'http://localhost:8000/openrtb2/auction',
+                    },
+                    timeout: 1000,
+                    debug: true,
+                    allowUnknownBidderCodes: true
+                }],
+            })
+
+            pbjs.addAdUnits(adUnits);
+            pbjs.requestBids({
+                bidsBackHandler: renderAllAdUnits
+            })
+        });
+    </script>
+
+</head>
+
+<body>
+    <h1>001_banner</h1>
+    <p class="description">
+        This demo uses Prebid.js to interact with Prebid Server to fill the ad slot <strong>test-div-1</strong>
+        The auction request to Prebid Server uses a stored request, which in turn links to a stored response.</br>
+        Look for the <strong>/auction</strong> request in your browser's developer tool to inspect the request
+        and response.
+    </p>
+    <h2>&#8595I am ad unit test-div-1 &#8595</h2>
+        <div id="test-div-1">
+        </div>
+</body>
+</html>
diff --git a/sample/001_banner/stored_request.json b/sample/001_banner/stored_request.json
new file mode 100644
index 00000000..59e13ae8
--- /dev/null
+++ b/sample/001_banner/stored_request.json
@@ -0,0 +1,28 @@
+{
+    "id": "test-imp-id",
+    "banner": {
+        "format": [
+            {
+                "w": 300,
+                "h": 250
+            },
+            {
+                "w": 300,
+                "h": 600
+            }
+        ]
+    },
+    "ext": {
+        "prebid": {
+            "bidder": {
+                "pubmatic": {
+                    "publisherId": "111111",
+                    "adSlot": "test"
+                }
+            },
+            "storedbidresponse": [
+               { "bidder": "pubmatic", "id": "test-bid-id" }
+            ]
+        }
+    }
+}
diff --git a/sample/001_banner/stored_response.json b/sample/001_banner/stored_response.json
new file mode 100644
index 00000000..033d9928
--- /dev/null
+++ b/sample/001_banner/stored_response.json
@@ -0,0 +1,46 @@
+{
+    "id": "test-auction-id",
+    "seatbid": [
+        {
+            "bid": [
+                {
+                    "id": "test-bid-id",
+                    "impid": "test-div-1",
+                    "price": 1,
+                    "adm": "<img src =\"http://files.prebid.org/creatives/prebid300x250.png\" />",
+                    "adomain": [
+                        "www.addomain.com"
+                    ],
+                    "iurl": "http://localhost11",
+                    "crid": "creative111",
+                    "w": 300,
+                    "h": 250,
+                    "mtype": 1,
+                    "ext": {
+                        "bidtype": 0,
+                        "dspid": 6,
+                        "origbidcpm": 1,
+                        "origbidcur": "USD",
+                        "prebid": {
+                            "meta": {
+                                "adaptercode": "pubmatic"
+                            },
+                            "targeting": {
+                                "hb_bidder": "pubmatic",
+                                "hb_pb": "1.00",
+                                "hb_size": "300x250"
+                            },
+                            "type": "banner",
+                            "video": {
+                                "duration": 0,
+                                "primary_category": ""
+                            }
+                        }
+                    }
+                }
+            ],
+            "seat": "pubmatic"
+        }
+    ],
+    "cur": "USD"
+}
diff --git a/sample/README.md b/sample/README.md
new file mode 100644
index 00000000..6b0109aa
--- /dev/null
+++ b/sample/README.md
@@ -0,0 +1,65 @@
+# Sample
+
+The Sample describes several demos of quickly spinning up different Prebid Server instances with various preset configurations. These samples are intended for audiences with little knowledge about Prebid Server and plan to play around with it locally and see how it works.
+
+# Installation
+
+In the Sample, we use `docker` and `docker-compose` to instantiate examples; with docker providing a unified setup and interface,  you can spin up a demo server instance locally with only one command without knowing all the complexities.
+The docker image used in `docker-compose.yml` is the `Dockerfile` residing in the root level of the repository. 
+
+## Option 1 - Standard Docker Engine
+Install `docker` and `docker-compose` via the [official docker page](https://docs.docker.com/compose/install/#scenario-one-install-docker-desktop). If you cannot use the official docker engine due to restrictions of its license, see the option below about using Podman instead of Docker. 
+
+## Option 2 - Podman
+From MacOS, you can use [podman](https://podman.io/) with these additional steps:
+
+```sh
+$ brew install podman docker-compose
+$ podman machine init
+$ podman machine set --rootful
+$ podman machine start
+```
+
+# Examples
+
+## Common File & Structures
+All required files for each example are stored in a folder that follows the name pattern <number>_<name>. The `<number>` suggests its order and `<name`>` describes its title.
+
+The following files will be present for every example and are exclusively catered to that example.
+1. `app.yaml` - the prebid server app config.
+2. `pbjs.html` - the HTML file with `Prebid JS` integration and communicates with the Prebid Server. It also provides a detailed explanation of the example.
+3. `*.json` - additional files required to support the example. e.g. stored request and stored response.
+
+## Common steps 
+
+```sh
+#1 - To get to the sample folder if you are on the root repository directory.
+$ cd sample 
+
+#2a - This command builds a new image, you should execute this command whenever the repository source code changes.
+$ docker-compose build
+
+#2b - Optionally you could run `docker-compose build --no-cache` if you want to build an completely new image without using cache but results in slower time to build it.
+$ docker-compose build --no-cache
+
+#3a - Spin up a corresponding sample in a container - see Steps for details 
+$ docker-compose up <number>_<name>
+
+#3b - Optionally you could use `--force-recreate` flag if you want to recreate the container every time you spin up the container.
+$ docker-compose up <number>_<name> --force-recreate
+```
+
+### Detailed Steps
+1. To prevent `app.yaml` from being overwritten by other config files. Ensure that `pbs.yaml` or `pbs.json` config file **MUST NOT** be present in the root directory of the repository.
+
+2. Bring up an instance by running `docker-compose up <number>_<name>` in the `sample` folder.
+
+3. Wait patiently until you see ` Admin server starting on: :6060` and `Main server starting on: :8000` in the command line output. This marks the Prebid Server instance finishing its initialization and is ready to serve the auction traffic.
+
+4. you can copy the URL `http://localhost:8000/status` and paste it into your browser. You should see `ok` in the response which is another way to tell the Prebid Server that the main auction server is up and running.
+
+5. Open a new tab in your browser and turn on the console UI. If you are using Chrome, you can right-click on the page and click `inspect`. Once the console UI is on, click on the `Network` tab to inspect the traffic later.
+
+6. Copy the URL `http://localhost:8000/static/pbjs.html?pbjs_debug=true` into your browser. It starts the example immediately with debugging information from `Prebid JS`, and you can inspect the request and response between `Prebid JS` and `Prebid Server`.
+
+7. After playing with the example, type `docker-compose down`. This is to shut down the existing Sample so you can start the next one you want to select.
diff --git a/sample/docker-compose.yml b/sample/docker-compose.yml
new file mode 100644
index 00000000..49b615c1
--- /dev/null
+++ b/sample/docker-compose.yml
@@ -0,0 +1,20 @@
+version: "3.9"
+services:
+  001_banner:
+    platform: linux/amd64
+    build:
+      context: ../
+      dockerfile: Dockerfile
+      args:
+        - TEST=false
+    image: pbs-sample
+    container_name: 001_banner
+    privileged: true
+    ports:
+      - "8000:8000"
+      - "6060:6060"
+    volumes:
+      - ../sample/001_banner/app.yaml:/usr/local/bin/pbs.yaml
+      - ../sample/001_banner/pbjs.html:/usr/local/bin/static/pbjs.html
+      - ../sample/001_banner/stored_request.json:/usr/local/bin/stored_requests/data/by_id/stored_imps/test-imp-id.json
+      - ../sample/001_banner/stored_response.json:/usr/local/bin/stored_responses/data/by_id/stored_responses/test-bid-id.json
\ No newline at end of file
diff --git a/static/bidder-info/cpmstar.yaml b/static/bidder-info/cpmstar.yaml
index 7c54ba53..94a91e5a 100644
--- a/static/bidder-info/cpmstar.yaml
+++ b/static/bidder-info/cpmstar.yaml
@@ -11,6 +11,9 @@ capabilities:
       - banner
       - video
 userSync:
+  iframe:
+    url: "https://server.cpmstar.com/usersync.aspx?ifr=1&gdpr={{.GDPR}}&consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect={{.RedirectURL}}"
+    userMacro: "$UID"
   redirect:
     url: "https://server.cpmstar.com/usersync.aspx?gdpr={{.GDPR}}&consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect={{.RedirectURL}}"
-    userMacro: "$UID"
+    userMacro: "$UID"    
\ No newline at end of file
diff --git a/static/bidder-info/jdpmedia.yaml b/static/bidder-info/jdpmedia.yaml
new file mode 100644
index 00000000..8139dd4f
--- /dev/null
+++ b/static/bidder-info/jdpmedia.yaml
@@ -0,0 +1,5 @@
+endpoint: "http://jdpmedia-prebid.smart-hub.io/pbserver/?seat={{.AccountID}}&token={{.SourceId}}"
+aliasOf: "smarthub"
+userSync:
+  supports:
+    - redirect
\ No newline at end of file
diff --git a/static/bidder-info/mabidder.yaml b/static/bidder-info/mabidder.yaml
index 3f03fd87..b66e9a8b 100644
--- a/static/bidder-info/mabidder.yaml
+++ b/static/bidder-info/mabidder.yaml
@@ -8,4 +8,7 @@ capabilities:
       - banner
   site:
     mediaTypes:
-      - banner
\ No newline at end of file
+      - banner
+# This bidder does not operate globally. Please consider setting "disabled: true" outside of the following regions:
+geoscope:
+  - CAN
\ No newline at end of file
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


Test patch:
diff --git a/adapters/medianet/medianettest/exemplary/multi-imps.json b/adapters/medianet/medianettest/exemplary/multi-imps.json
index 3aca4c1d..c48c4fdd 100644
--- a/adapters/medianet/medianettest/exemplary/multi-imps.json
+++ b/adapters/medianet/medianettest/exemplary/multi-imps.json
@@ -100,7 +100,8 @@
                                     "adm": "some-test-ad",
                                     "crid": "test-crid",
                                     "h": 50,
-                                    "w": 320
+                                    "w": 320,
+                                    "mtype": 1
                                 }
                             ]
                         }
@@ -123,7 +124,8 @@
                         "adm": "some-test-ad",
                         "crid": "test-crid",
                         "w": 320,
-                        "h": 50
+                        "h": 50,
+                        "mtype": 1
                     },
                     "type": "banner"
                 }
diff --git a/adapters/medianet/medianettest/exemplary/simple-banner.json b/adapters/medianet/medianettest/exemplary/simple-banner.json
index cff32e27..87ff3ec1 100644
--- a/adapters/medianet/medianettest/exemplary/simple-banner.json
+++ b/adapters/medianet/medianettest/exemplary/simple-banner.json
@@ -66,7 +66,8 @@
                                     "adm": "some-test-ad",
                                     "crid": "test-crid",
                                     "h": 50,
-                                    "w": 320
+                                    "w": 320,
+                                    "mtype": 1
                                 }
                             ]
                         }
@@ -89,7 +90,8 @@
                         "adm": "some-test-ad",
                         "crid": "test-crid",
                         "w": 320,
-                        "h": 50
+                        "h": 50,
+                        "mtype": 1
                     },
                     "type": "banner"
                 }
diff --git a/adapters/medianet/medianettest/exemplary/simple-video.json b/adapters/medianet/medianettest/exemplary/simple-video.json
index ffd0a0ac..8f7f77fb 100644
--- a/adapters/medianet/medianettest/exemplary/simple-video.json
+++ b/adapters/medianet/medianettest/exemplary/simple-video.json
@@ -71,7 +71,8 @@
                                     "adm": "some-test-ad",
                                     "crid": "test-crid",
                                     "w": 320,
-                                    "h": 480
+                                    "h": 480,
+                                    "mtype": 2
                                 }
                             ]
                         }
@@ -92,7 +93,8 @@
                         "adm": "some-test-ad",
                         "crid": "test-crid",
                         "w": 320,
-                        "h": 480
+                        "h": 480,
+                        "mtype": 2
                     },
                     "type": "video"
                 }
diff --git a/adapters/medianet/medianettest/supplemental/valid-req-200-bid-response-from-mnet.json b/adapters/medianet/medianettest/supplemental/valid-req-200-bid-response-from-mnet.json
index 99ac6cb8..61da5b31 100644
--- a/adapters/medianet/medianettest/supplemental/valid-req-200-bid-response-from-mnet.json
+++ b/adapters/medianet/medianettest/supplemental/valid-req-200-bid-response-from-mnet.json
@@ -101,7 +101,8 @@
                 "cid": "987",
                 "crid": "12345678",
                 "h": 250,
-                "w": 300
+                "w": 300,
+                "mtype": 1
               }
             ]
           }
@@ -123,7 +124,8 @@
         "cid": "987",
         "crid": "12345678",
         "h": 250,
-        "w": 300
+        "w": 300,
+        "mtype": 1
       },
       "type": "banner"
     }]
diff --git a/adapters/pubmatic/pubmatictest/supplemental/impExtData.json b/adapters/pubmatic/pubmatictest/supplemental/impExt.json
similarity index 97%
rename from adapters/pubmatic/pubmatictest/supplemental/impExtData.json
rename to adapters/pubmatic/pubmatictest/supplemental/impExt.json
index 17bb47fe..368dd2fa 100644
--- a/adapters/pubmatic/pubmatictest/supplemental/impExtData.json
+++ b/adapters/pubmatic/pubmatictest/supplemental/impExt.json
@@ -32,7 +32,8 @@
                             "rugby",
                             "cricket"
                         ]
-                    }
+                    },
+                    "gpid":"/1111/home"
                 }
             }
         ],
@@ -68,7 +69,8 @@
                             },
                             "ext": {
                                 "dfp_ad_unit_code": "/1111/home",
-                                "key_val": "k1=v1|k2=v2|sport=rugby,cricket"
+                                "key_val": "k1=v1|k2=v2|sport=rugby,cricket",
+                                "gpid":"/1111/home"
                             }
                         }
                     ],
diff --git a/adapters/yeahmobi/yeahmobitest/exemplary/simple-video.json b/adapters/yeahmobi/yeahmobitest/exemplary/simple-video.json
index cda8ebe4..af07de8f 100644
--- a/adapters/yeahmobi/yeahmobitest/exemplary/simple-video.json
+++ b/adapters/yeahmobi/yeahmobitest/exemplary/simple-video.json
@@ -20,7 +20,6 @@
       }
     ]
   },
-
   "httpCalls": [
     {
       "expectedRequest": {
@@ -29,7 +28,7 @@
           "id": "test-request-id",
           "imp": [
             {
-              "id":"test-imp-id",
+              "id": "test-imp-id",
               "video": {
                 "w": 300,
                 "h": 250,
@@ -46,7 +45,9 @@
             }
           ]
         },
-        "impIDs":["test-imp-id"]
+        "impIDs": [
+          "test-imp-id"
+        ]
       },
       "mockResponse": {
         "status": 200,
@@ -55,13 +56,20 @@
           "seatbid": [
             {
               "seat": "ttx",
-              "bid": [{
-                "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800",
-                "impid": "test-imp-id",
-                "price": 1.2,
-                "adm": "some-ads",
-                "crid": "crid_testid"
-              }]
+              "bid": [
+                {
+                  "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800",
+                  "impid": "test-imp-id",
+                  "price": 1.2,
+                  "adm": "some-ads",
+                  "crid": "crid_testid",
+                  "ext": {
+                    "video": {
+                      "duration": 300
+                    }
+                  }
+                }
+              ]
             }
           ],
           "cur": "USD"
@@ -69,7 +77,6 @@
       }
     }
   ],
-
   "expectedBidResponses": [
     {
       "currency": "USD",
@@ -80,9 +87,17 @@
             "impid": "test-imp-id",
             "price": 1.2,
             "adm": "some-ads",
-            "crid": "crid_testid"
+            "crid": "crid_testid",
+            "ext": {
+              "video": {
+                "duration": 300
+              }
+            }
           },
-          "type": "video"
+          "type": "video",
+          "video": {
+            "duration": 300
+          }
         }
       ]
     }
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..b3e97b4d 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"
 )
@@ -101,7 +102,7 @@ func BenchmarkOpenrtbEndpoint(b *testing.B) {
 	endpoint, _ := NewEndpoint(
 		fakeUUIDGenerator{},
 		exchange,
-		paramValidator,
+		ortb.NewRequestValidator(nil, map[string]string{}, paramValidator),
 		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..0521befa 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"
@@ -1259,6 +1260,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{}
 
@@ -1303,7 +1305,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 +1317,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/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)
+			}
+		})
+	}
+}