diff --git a/adapters/tradplus/params_test.go b/adapters/tradplus/params_test.go new file mode 100644 index 0000000000..1b1f48a2fe --- /dev/null +++ b/adapters/tradplus/params_test.go @@ -0,0 +1,54 @@ +package tradplus + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/v3/openrtb_ext" +) + +func TestValidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json-schemas. %v", err) + } + + for _, validParam := range validParams { + if err := validator.Validate(openrtb_ext.BidderTradPlus, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected tradplus params: %s", validParam) + } + } +} + +// TestInvalidParams makes sure that the tradplus schema rejects all the imp.ext fields we don't support. +func TestInvalidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json-schemas. %v", err) + } + + for _, invalidParam := range invalidParams { + if err := validator.Validate(openrtb_ext.BidderTradPlus, json.RawMessage(invalidParam)); err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } +} + +var validParams = []string{ + `{"accountId": "11233", "zoneId": ""}`, + `{"accountId": "aaa", "accountId": "us"}`, + `{"accountId": "aa", "accountId": "sin"}`, +} + +var invalidParams = []string{ + `{"accountId": ""}`, + `{"accountId": "", "zoneId": ""}`, + `{"accountId": "", "zoneId": "sin"}`, + `{"accountId": 123}`, + `{"accountId": {"test":1}}`, + `{"accountId": true}`, + `{"accountId": null}`, + `{"zoneId": "aaa"}`, + `{"zoneId": "aaa"}`, + `{"zoneId": null}`, +} diff --git a/adapters/tradplus/tradplus.go b/adapters/tradplus/tradplus.go new file mode 100644 index 0000000000..c3277f96c1 --- /dev/null +++ b/adapters/tradplus/tradplus.go @@ -0,0 +1,141 @@ +package tradplus + +import ( + "encoding/json" + "fmt" + "net/http" + "text/template" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v3/adapters" + "github.com/prebid/prebid-server/v3/config" + "github.com/prebid/prebid-server/v3/errortypes" + "github.com/prebid/prebid-server/v3/macros" + "github.com/prebid/prebid-server/v3/openrtb_ext" + "github.com/prebid/prebid-server/v3/util/jsonutil" +) + +type adapter struct { + endpoint *template.Template +} + +// Builder builds a new instance of the tradplus adapter for the given bidder with the given config. +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + template, err := template.New("endpointTemplate").Parse(config.Endpoint) + if err != nil { + return nil, fmt.Errorf("unable to parse endpoint url template: %v", err) + } + + return &adapter{ + endpoint: template, + }, nil +} + +func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + adapterRequest, errs := a.makeRequest(request) + if errs != nil { + return nil, errs + } + return []*adapters.RequestData{adapterRequest}, nil +} + +func (a *adapter) makeRequest(request *openrtb2.BidRequest) (*adapters.RequestData, []error) { + + tradplusExt, err := getImpressionExt(&request.Imp[0]) + if err != nil { + return nil, []error{err} + } + + request.Imp[0].Ext = nil + + url, err := a.buildEndpointURL(tradplusExt) + if err != nil { + return nil, []error{err} + } + + reqBody, err := jsonutil.Marshal(request) + if err != nil { + return nil, []error{err} + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + return &adapters.RequestData{ + Method: "POST", + Uri: url, + Body: reqBody, + Headers: headers, + ImpIDs: openrtb_ext.GetImpIDs(request.Imp), + }, nil +} + +func getImpressionExt(imp *openrtb2.Imp) (*openrtb_ext.ExtImpTradPlus, error) { + var bidderExt adapters.ExtImpBidder + if err := jsonutil.Unmarshal(imp.Ext, &bidderExt); err != nil { + return nil, &errortypes.BadInput{ + Message: "Error parsing tradplusExt - " + err.Error(), + } + } + + var tradplusExt openrtb_ext.ExtImpTradPlus + if err := jsonutil.Unmarshal(bidderExt.Bidder, &tradplusExt); err != nil { + return nil, &errortypes.BadInput{ + Message: "Error parsing bidderExt - " + err.Error(), + } + } + + return &tradplusExt, nil +} + +func (a *adapter) buildEndpointURL(params *openrtb_ext.ExtImpTradPlus) (string, error) { + endpointParams := macros.EndpointTemplateParams{ + AccountID: params.AccountID, + ZoneID: params.ZoneID, + } + return macros.ResolveMacros(a.endpoint, endpointParams) +} + +// MakeBids make the bids for the bid response. +func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if adapters.IsResponseStatusCodeNoContent(response) { + return nil, nil + } + if err := adapters.CheckResponseStatusCodeForErrors(response); err != nil { + return nil, []error{&errortypes.BadInput{ + 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} + } + var errs []error + bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(internalRequest.Imp)) + for _, sb := range bidResp.SeatBid { + for i := range sb.Bid { + mediaType, err := getMediaTypeForBid(sb.Bid[i]) + if err != nil { + errs = append(errs, err) + continue + } + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: &sb.Bid[i], + BidType: mediaType, + }) + } + } + return bidResponse, errs +} + +func getMediaTypeForBid(bid openrtb2.Bid) (openrtb_ext.BidType, error) { + switch bid.MType { + case openrtb2.MarkupBanner: + return openrtb_ext.BidTypeBanner, nil + case openrtb2.MarkupNative: + return openrtb_ext.BidTypeNative, nil + case openrtb2.MarkupVideo: + return openrtb_ext.BidTypeVideo, nil + default: + return "", fmt.Errorf("unrecognized bid type in response from tradplus for bid %s", bid.ImpID) + } +} diff --git a/adapters/tradplus/tradplus_test.go b/adapters/tradplus/tradplus_test.go new file mode 100644 index 0000000000..e081607f4e --- /dev/null +++ b/adapters/tradplus/tradplus_test.go @@ -0,0 +1,29 @@ +package tradplus + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/prebid/prebid-server/v3/adapters/adapterstest" + "github.com/prebid/prebid-server/v3/config" + "github.com/prebid/prebid-server/v3/openrtb_ext" +) + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder(openrtb_ext.BidderTradPlus, config.Adapter{ + Endpoint: "https://{{.ZoneID}}adx.tradplusad.com/{{.AccountID}}/pserver"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + + adapterstest.RunJSONBidderTest(t, "tradplustest", bidder) +} + +func TestEndpointTemplateMalformed(t *testing.T) { + _, buildErr := Builder(openrtb_ext.BidderTradPlus, config.Adapter{ + Endpoint: "{{Malformed}}"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + + assert.Error(t, buildErr) +} diff --git a/adapters/tradplus/tradplustest/exemplary/no-bid.json b/adapters/tradplus/tradplustest/exemplary/no-bid.json new file mode 100644 index 0000000000..96d51bf92f --- /dev/null +++ b/adapters/tradplus/tradplustest/exemplary/no-bid.json @@ -0,0 +1,53 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 50 + } + ] + }, + "ext": { + "bidder": { + "accountId": "fake-account-id", + "zoneId": "" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://adx.tradplusad.com/fake-account-id/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 50 + } + ] + } + } + ] + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 204, + "body": {} + } + } + ], + "expectedBidResponses": [] +} diff --git a/adapters/tradplus/tradplustest/exemplary/simple-banner.json b/adapters/tradplus/tradplustest/exemplary/simple-banner.json new file mode 100644 index 0000000000..6b9df19900 --- /dev/null +++ b/adapters/tradplus/tradplustest/exemplary/simple-banner.json @@ -0,0 +1,88 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 50 + } + ] + }, + "ext": { + "bidder": { + "accountId": "fake-account-id", + "zoneId": "us-" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://us-adx.tradplusad.com/fake-account-id/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 50 + } + ] + } + } + ] + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "ttx", + "bid": [ + { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 1.2, + "adm": "some-ads", + "crid": "crid_testid", + "mtype": 1 + } + ] + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 1.2, + "adm": "some-ads", + "crid": "crid_testid", + "mtype": 1 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/tradplus/tradplustest/exemplary/simple-native.json b/adapters/tradplus/tradplustest/exemplary/simple-native.json new file mode 100644 index 0000000000..6dd785a753 --- /dev/null +++ b/adapters/tradplus/tradplustest/exemplary/simple-native.json @@ -0,0 +1,78 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "native": { + "request": "{\"ver\":\"1.2\",\"context\":1,\"plcmttype\":4,\"plcmtcnt\":1,\"assets\":[{\"id\":2,\"required\":1,\"title\":{\"len\":90}},{\"id\":6,\"required\":1,\"img\":{\"type\":3,\"wmin\":128,\"hmin\":128,\"mimes\":[\"image/jpg\",\"image/jpeg\",\"image/png\"]}},{\"id\":7,\"required\":1,\"data\":{\"type\":2,\"len\":120}}]}", + "ver": "1.2" + }, + "ext": { + "bidder": { + "accountId": "fake-account-id", + "zoneId": "" + } + } + } + ] + }, + "httpcalls": [ + { + "expectedRequest": { + "uri": "https://adx.tradplusad.com/fake-account-id/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "native": { + "request": "{\"ver\":\"1.2\",\"context\":1,\"plcmttype\":4,\"plcmtcnt\":1,\"assets\":[{\"id\":2,\"required\":1,\"title\":{\"len\":90}},{\"id\":6,\"required\":1,\"img\":{\"type\":3,\"wmin\":128,\"hmin\":128,\"mimes\":[\"image/jpg\",\"image/jpeg\",\"image/png\"]}},{\"id\":7,\"required\":1,\"data\":{\"type\":2,\"len\":120}}]}", + "ver": "1.2" + } + } + ] + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "8400d766-58b3-47d4-80d7-6658b337d403", + "impid": "test-imp-id", + "price": 1.2, + "adm": "some ads", + "crid": "crid_testid", + "mtype": 4 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "8400d766-58b3-47d4-80d7-6658b337d403", + "impid": "test-imp-id", + "price": 1.2, + "adm": "some ads", + "crid": "crid_testid", + "mtype": 4 + }, + "type": "native" + } + ] + } + ] +} diff --git a/adapters/tradplus/tradplustest/exemplary/simple-video.json b/adapters/tradplus/tradplustest/exemplary/simple-video.json new file mode 100644 index 0000000000..7032830635 --- /dev/null +++ b/adapters/tradplus/tradplustest/exemplary/simple-video.json @@ -0,0 +1,86 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "w": 300, + "h": 250, + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "bidder": { + "accountId": "fake-account-id", + "zoneId": "" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://adx.tradplusad.com/fake-account-id/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "w": 300, + "h": 250, + "mimes": [ + "video/mp4" + ] + } + } + ] + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "ttx", + "bid": [ + { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 1.2, + "adm": "some-ads", + "crid": "crid_testid", + "mtype": 2 + } + ] + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 1.2, + "adm": "some-ads", + "crid": "crid_testid", + "mtype": 2 + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/tradplus/tradplustest/supplemental/bad_imp_ext.json b/adapters/tradplus/tradplustest/supplemental/bad_imp_ext.json new file mode 100644 index 0000000000..b0e7b7d3c8 --- /dev/null +++ b/adapters/tradplus/tradplustest/supplemental/bad_imp_ext.json @@ -0,0 +1,21 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [{"w": 300, "h": 50}] + }, + "ext": "aaa" + } + ] + }, + + "expectedMakeRequestsErrors": [ + { + "value": "Error parsing tradplusExt - expect { or n, but found \"", + "comparison": "literal" + } + ] +} diff --git a/adapters/tradplus/tradplustest/supplemental/bad_imp_ext_bidder.json b/adapters/tradplus/tradplustest/supplemental/bad_imp_ext_bidder.json new file mode 100644 index 0000000000..1c3ad6351d --- /dev/null +++ b/adapters/tradplus/tradplustest/supplemental/bad_imp_ext_bidder.json @@ -0,0 +1,27 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 50 + } + ] + }, + "ext": { + "bidder": "aa" + } + } + ] + }, + "expectedMakeRequestsErrors": [ + { + "value": "Error parsing bidderExt - expect { or n, but found \"", + "comparison": "literal" + } + ] +} diff --git a/adapters/tradplus/tradplustest/supplemental/bad_response.json b/adapters/tradplus/tradplustest/supplemental/bad_response.json new file mode 100644 index 0000000000..29ef512126 --- /dev/null +++ b/adapters/tradplus/tradplustest/supplemental/bad_response.json @@ -0,0 +1,58 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 50 + } + ] + }, + "ext": { + "bidder": { + "accountId": "fake-account-id", + "zoneId": "" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://adx.tradplusad.com/fake-account-id/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 50 + } + ] + } + } + ] + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": "{\"id\":test-request-id" + } + } + ], + "expectedMakeBidsErrors": [ + { + "comparison": "literal", + "value": "json: cannot unmarshal string into Go value of type openrtb2.BidResponse" + } + ] +} diff --git a/adapters/tradplus/tradplustest/supplemental/status_400.json b/adapters/tradplus/tradplustest/supplemental/status_400.json new file mode 100644 index 0000000000..0cc859a7cd --- /dev/null +++ b/adapters/tradplus/tradplustest/supplemental/status_400.json @@ -0,0 +1,58 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 50 + } + ] + }, + "ext": { + "bidder": { + "accountId": "fake-account-id", + "zoneId": "" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://adx.tradplusad.com/fake-account-id/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 50 + } + ] + } + } + ] + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 400, + "body": {} + } + } + ], + "expectedMakeBidsErrors": [ + { + "comparison": "literal", + "value": "Unexpected status code: 400." + } + ] +} diff --git a/adapters/tradplus/tradplustest/supplemental/status_500.json b/adapters/tradplus/tradplustest/supplemental/status_500.json new file mode 100644 index 0000000000..d3f08b4e17 --- /dev/null +++ b/adapters/tradplus/tradplustest/supplemental/status_500.json @@ -0,0 +1,58 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 50 + } + ] + }, + "ext": { + "bidder": { + "accountId": "fake-account-id", + "zoneId": "" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://adx.tradplusad.com/fake-account-id/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 50 + } + ] + } + } + ] + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 500, + "body": {} + } + } + ], + "expectedMakeBidsErrors": [ + { + "comparison": "literal", + "value": "Unexpected status code: 500." + } + ] +} diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go index d1d6a87fb2..824caebdc0 100755 --- a/exchange/adapter_builders.go +++ b/exchange/adapter_builders.go @@ -199,6 +199,7 @@ import ( "github.com/prebid/prebid-server/v3/adapters/theadx" "github.com/prebid/prebid-server/v3/adapters/thetradedesk" "github.com/prebid/prebid-server/v3/adapters/tpmn" + "github.com/prebid/prebid-server/v3/adapters/tradplus" "github.com/prebid/prebid-server/v3/adapters/trafficgate" "github.com/prebid/prebid-server/v3/adapters/triplelift" "github.com/prebid/prebid-server/v3/adapters/triplelift_native" @@ -434,6 +435,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderTheadx: theadx.Builder, openrtb_ext.BidderTheTradeDesk: thetradedesk.Builder, openrtb_ext.BidderTpmn: tpmn.Builder, + openrtb_ext.BidderTradPlus: tradplus.Builder, openrtb_ext.BidderTrafficGate: trafficgate.Builder, openrtb_ext.BidderTriplelift: triplelift.Builder, openrtb_ext.BidderTripleliftNative: triplelift_native.Builder, diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index 3f6b0a6703..359465f499 100644 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -217,6 +217,7 @@ var coreBidderNames []BidderName = []BidderName{ BidderTheadx, BidderTheTradeDesk, BidderTpmn, + BidderTradPlus, BidderTrafficGate, BidderTriplelift, BidderTripleliftNative, @@ -556,6 +557,7 @@ const ( BidderTheadx BidderName = "theadx" BidderTheTradeDesk BidderName = "thetradedesk" BidderTpmn BidderName = "tpmn" + BidderTradPlus BidderName = "tradplus" BidderTrafficGate BidderName = "trafficgate" BidderTriplelift BidderName = "triplelift" BidderTripleliftNative BidderName = "triplelift_native" diff --git a/openrtb_ext/imp_tradplus.go b/openrtb_ext/imp_tradplus.go new file mode 100644 index 0000000000..4f7139c88b --- /dev/null +++ b/openrtb_ext/imp_tradplus.go @@ -0,0 +1,7 @@ +package openrtb_ext + +// ExtImpTradPlus defines the contract for bidrequest.imp[i].ext.prebid.bidder.tradplus +type ExtImpTradPlus struct { + AccountID string `json:"accountId"` + ZoneID string `json:"zoneId"` +} diff --git a/static/bidder-info/tradplus.yaml b/static/bidder-info/tradplus.yaml new file mode 100644 index 0000000000..02d3e67ea1 --- /dev/null +++ b/static/bidder-info/tradplus.yaml @@ -0,0 +1,15 @@ +endpoint: "https://{{.ZoneID}}adx.tradplusad.com/{{.AccountID}}/pserver" + +maintainer: + email: "tpxcontact@tradplus.com" +capabilities: + app: + mediaTypes: + - banner + - video + - native + site: + mediaTypes: + - banner + - native + - video \ No newline at end of file diff --git a/static/bidder-params/tradplus.json b/static/bidder-params/tradplus.json new file mode 100644 index 0000000000..2f568bad07 --- /dev/null +++ b/static/bidder-params/tradplus.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "TradPlus Adapter Params", + "description": "A schema which validates params accepted by the TradPlus adapter", + "type": "object", + "properties": { + "accountId": { + "type": "string", + "description": "Account ID", + "minLength": 1 + }, + "zoneId": { + "type": "string", + "description": "Zone ID" + } + }, + "required": [ + "accountId" + ] +} \ No newline at end of file