Skip to content

New Adapter: Nativery#4223

Merged
CTMBNara merged 11 commits into
masterfrom
New-Adapter-Nativery
Nov 18, 2025
Merged

New Adapter: Nativery#4223
CTMBNara merged 11 commits into
masterfrom
New-Adapter-Nativery

Conversation

@przemkaczmarek
Copy link
Copy Markdown
Collaborator

🔧 Type of changes

  • new bid adapter

✨ What's the context?

#4198

@przemkaczmarek przemkaczmarek self-assigned this Oct 2, 2025
@osulzhenko osulzhenko linked an issue Oct 3, 2025 that may be closed by this pull request
- video
- native
supported-vendors: []
vendor-id: 0
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1133

- banner
- video
- native
supported-vendors: []
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

leave it just as supported-vendors:

Comment on lines +31 to +33
BidderDeps nativeryBidderDeps(BidderConfigurationProperties nativeryConfigurationProperties,
@NotBlank @Value("${external-url}") String externalUrl,
JacksonMapper mapper) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please format

return mapper.mapper().convertValue(root, ExtRequest.class);
}

private boolean isAmpRequest(BidRequest request) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix the methods order

for (Imp imp : request.getImp()) {
try {
final ExtImpNativery extImp = parseImpExt(imp);
if (widgetId == null && extImp != null && StringUtils.isNotBlank(extImp.getWidgetId())) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extImp cannot be null at this stage

final BidRequest singleImpRequest = request.toBuilder()
.imp(Collections.singletonList(imp))
.ext(updatedExt)
.cur(Collections.singletonList(DEFAULT_CURRENCY))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this change needed? I don't this in the Go version

Comment on lines +141 to +148
if (httpCall.getResponse() != null && httpCall.getResponse().getStatusCode() == 204) {
final MultiMap headers = httpCall.getResponse().getHeaders();
final String nativeryErr = headers != null ? headers.get(NATIVERY_ERROR_HEADER) : null;
if (StringUtils.isNotBlank(nativeryErr)) {
return Result.withError(BidderError.badInput("Nativery Error: " + nativeryErr + "."));
}
return Result.withError(BidderError.badServerResponse("No Content"));
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately this isn't going to work because the 204 status code is check before calling makeBids in the core, so probably we won't support this weird error-header approach

@CTMBNara what do you think?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Y, it won't work

Comment thread src/main/java/org/prebid/server/bidder/nativery/NativeryBidder.java Outdated

private BidderBid resolveBidderBid(Bid bid, String currency, List<BidderError> errors) {
try {
final ObjectNode nativeryExt = extractNativeryExt(bid.getExt());
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's create a separate BidExtNativery object and deserialize with the object mapper, because that manual conversion feels bad

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^^

Comment on lines +193 to +202
private ObjectNode extractNativeryExt(ObjectNode bidExt) {
if (bidExt == null) {
throw new PreBidException("missing bid.ext");
}
final JsonNode node = bidExt.get("nativery");
if (!(node instanceof ObjectNode nativeryNode)) {
throw new PreBidException("missing bid.ext.nativery");
}
return nativeryNode;
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace with a simple Optional chain

return Result.of(Collections.emptyList(), errors);
}

// 🟢 Zmienione — zamieniamy ExtRequest na ObjectNode
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please remove the comments

}

if (validImps.isEmpty()) {
return Result.of(Collections.emptyList(), errors);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Result.withErrors(errors)

Comment on lines +86 to +88
final ObjectNode originalExt = request.getExt() != null
? mapper.mapper().convertValue(request.getExt(), ObjectNode.class)
: null;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's not necessary to serialize ExtRequest object into the ObjectNode, because ExtRequest is a FlexibleExtension

Comment on lines +117 to +119
final ExtPrebid<?, ExtImpNativery> ext =
mapper.mapper().convertValue(imp.getExt(), NATIVERY_EXT_TYPE_REFERENCE);
return ext != null ? ext.getBidder() : null;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imp.ext can't be null at this stage, so it can't be deserialized into null

Comment on lines +125 to +138
private ExtRequest buildRequestExtWithNativery(ObjectNode originalExt, boolean isAmp, String widgetId) {
final ObjectNode root = originalExt != null
? originalExt.deepCopy()
: mapper.mapper().createObjectNode();

final ObjectNode nativeryNode = root.with("nativery");

nativeryNode.put("isAmp", isAmp);
if (widgetId != null) {
nativeryNode.put("widgetId", widgetId);
}

return mapper.mapper().convertValue(root, ExtRequest.class);
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please use ExtRequest and FlexibleExtension capabilities

Comment thread src/main/java/org/prebid/server/bidder/nativery/NativeryBidder.java

private BidderBid resolveBidderBid(Bid bid, String currency, List<BidderError> errors) {
try {
final ObjectNode nativeryExt = extractNativeryExt(bid.getExt());
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^^

Comment thread src/main/java/org/prebid/server/bidder/nativery/NativeryBidder.java
Comment on lines +141 to +148
if (httpCall.getResponse() != null && httpCall.getResponse().getStatusCode() == 204) {
final MultiMap headers = httpCall.getResponse().getHeaders();
final String nativeryErr = headers != null ? headers.get(NATIVERY_ERROR_HEADER) : null;
if (StringUtils.isNotBlank(nativeryErr)) {
return Result.withError(BidderError.badInput("Nativery Error: " + nativeryErr + "."));
}
return Result.withError(BidderError.badServerResponse("No Content"));
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Y, it won't work

Comment thread src/main/java/org/prebid/server/bidder/nativery/NativeryBidder.java Outdated
Comment thread src/main/java/org/prebid/server/bidder/nativery/NativeryBidder.java Outdated
Comment thread src/main/java/org/prebid/server/bidder/nativery/NativeryBidder.java Outdated
Comment thread src/main/java/org/prebid/server/bidder/nativery/NativeryBidder.java Outdated
Comment thread src/main/java/org/prebid/server/bidder/nativery/NativeryBidder.java Outdated
Comment on lines +141 to +144
final var response = httpCall.getResponse();
if (response == null || StringUtils.isBlank(response.getBody())) {
return Result.withError(BidderError.badServerResponse("Empty response"));
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for this check

Comment on lines +178 to +179
final List<String> advDomains = ListUtils.defaultIfNull(
nativeryExt.getBidAdvDomains(), Collections.emptyList());
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I made a typo and recommended the wrong method. Use ListUtils.emptyIfNull() directly.

Comment on lines +215 to +222
private static String mediaTypeString(BidType type) {
return switch (type) {
case banner -> type.getName();
case video -> type.getName();
case xNative -> type.getName();
default -> throw new IllegalStateException("Unexpected value: " + type.getName());
};
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this method and use type.getName instead

Comment on lines +227 to +228
final List<String> safeAdvDomains = Optional.ofNullable(advDomains)
.orElse(Collections.emptyList());
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

advDomains can't be null

Comment on lines +11 to +15
@JsonProperty("bid_ad_media_type")
String bidAdMediaType;

@JsonProperty("bid_adv_domains")
List<String> bidAdvDomains;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for @JsonProperty. Our objectMapper uses snake-case by default

Comment on lines +133 to +134
final ObjectNode extNode = mapper.createObjectNode();
extNode.put("accountId", "acc-123");
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ObjectNode -> ExtRequest. You can add custom properties to it since it is a FlexibleExtension

Comment on lines +164 to +169
final ObjectNode extNode = mapper.createObjectNode();
final ObjectNode prebidNode = mapper.createObjectNode();
final ObjectNode serverNode = mapper.createObjectNode();
serverNode.put("endpoint", Endpoint.openrtb2_amp.value());
prebidNode.set("server", serverNode);
extNode.set("prebid", prebidNode);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same

@And1sS And1sS requested a review from AntoxaAntoxic November 5, 2025 11:56
Comment on lines +105 to +108
final ExtRequest requestExt = bidRequest.getExt();
final ExtRequestPrebid prebid = requestExt != null ? requestExt.getPrebid() : null;
final ExtRequestPrebidServer server = prebid != null ? prebid.getServer() : null;
return server != null ? server.getEndpoint() : null;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please use Optional here

public Result<List<BidderBid>> makeBids(BidderCall<BidRequest> httpCall, BidRequest bidRequest) {
final List<BidderError> errors = new ArrayList<>();

final var response = httpCall.getResponse();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please don't use var

Comment on lines +185 to +198
private BidExtNativery parseNativeryExt(ObjectNode bidExt) {
if (bidExt == null) {
throw new PreBidException("missing bid.ext");
}
final JsonNode node = bidExt.get("nativery");
if (!(node instanceof ObjectNode nativeryNode)) {
throw new PreBidException("missing bid.ext.nativery");
}
try {
return mapper.mapper().convertValue(nativeryNode, BidExtNativery.class);
} catch (IllegalArgumentException e) {
throw new PreBidException("invalid bid.ext.nativery: " + e.getMessage());
}
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd simplify it to the optional chain like

return Optional.ofNullable(bidExt)
             .map(ext -> ext.get("nativery"))
             .filter(JsonNode::isObject)
             .map(/*convertValue*/)
             .orElseThrow(() -> new PreBidException("missing bid.ext.nativery"));

Comment on lines +89 to +93
assertThat(result.getValue())
.extracting(HttpRequest::getPayload)
.flatExtracting(BidRequest::getImp)
.extracting(Imp::getId)
.containsExactly("imp1");
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the assert is missing, it should check the request.payload.impIds instead

actually you can merge this test with the makeHttpRequestsShouldMakeOneRequestPerImp

public void makeHttpRequestsShouldPreserveOriginalExtFields() {
// given
final ExtRequest extRequest = ExtRequest.empty();
extRequest.addProperty("accountId", mapper.convertValue("acc-123", JsonNode.class));
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extRequest.addProperty("accountId", TextNode.valueOf("acc-123"));

Comment on lines +154 to +155
final ObjectNode serverNode = mapper.createObjectNode();
serverNode.put("endpoint", Endpoint.openrtb2_amp.value());
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is a ExtRequestPrebidServer class that can be used

final ObjectNode extNode = mapper.createObjectNode();
extNode.set("prebid", prebidNode);

final ExtRequest extRequest = mapper.convertValue(extNode, ExtRequest.class);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just build the ExtRequest object - no need to convert

Comment thread src/test/java/org/prebid/server/bidder/nativery/NativeryBidderTest.java Outdated
Comment on lines +312 to +314
assertThat(result.getValue())
.extracting(BidderBid::getType)
.containsExactly(BidType.xNative);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ideally the whole BidderBid object should be checked

.hasSize(1)
.allSatisfy(error -> {
assertThat(error.getMessage())
.contains("unrecognized bid_ad_media_type in response from nativery: audio");
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isEqualTo

Comment on lines +193 to +197
try {
return mapper.mapper().convertValue(node, BidExtNativery.class);
} catch (IllegalArgumentException e) {
throw new PreBidException("invalid bid.ext.nativery: " + e.getMessage());
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please extract into a separate method

}

private BidExtNativery parseNativeryExt(ObjectNode bidExt) {
return Optional.of(bidExt)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the last time there was a non-null check, why it's not here anymore?

Comment on lines +292 to +294
assertThat(result.getValue())
.extracting(BidderBid::getType)
.containsExactly(BidType.banner);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's better to assert the whole BidderBid object

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can’t write:
.containsExactly(BidderBid.of(Bid.builder().impid("123").build(), video, "EUR"));

because
the production code in NativeryBidder.makeBids always overrides bid.ext, setting it to a structure like:
{
"prebid": {
"meta": {
"mediaType": ...,
"advertiserDomains": [...]
}
}
}
The expected object has ext == null, so containsExactly(...) compares the full objects and detects a difference in the bid.ext field.
To make the assertion pass, you would need to construct the expected Bid with the same ext, or narrow the assertion to only check the type.

That’s why I used:
assertThat(result.getValue())
.extracting(BidderBid::getType)
.containsExactly(BidType.banner);

(Other adapters use this approach too, for example playdigo.)

Copy link
Copy Markdown
Collaborator

@AntoxaAntoxic AntoxaAntoxic Nov 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The goal of the test should be checking the bid is correct, not just a type of this bid. So narrowing the assertion might hide the potential issues. IMHO

Comment on lines +193 to +197
try {
return mapper.mapper().convertValue(node, BidExtNativery.class);
} catch (IllegalArgumentException e) {
throw new PreBidException("invalid bid.ext.nativery: " + e.getMessage());
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please extract into a separate method

}

private BidExtNativery parseNativeryExt(ObjectNode bidExt) {
return Optional.of(bidExt)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the last time there was a non-null check, why it's not here anymore?

Comment on lines +296 to +311
final Bid expectedBid = Bid.builder()
.impid("123")
.ext(mapper.valueToTree(ExtPrebid.of(
ExtBidPrebid.builder()
.meta(org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidMeta.builder()
.mediaType("banner")
.advertiserDomains(List.of())
.build())
.build(),
null)))
.build();

final BidderBid expected = BidderBid.of(expectedBid, banner, DEFAULT_CURRENCY);

assertThat(result.getErrors()).isEmpty();
assertThat(result.getValue()).containsExactly(expected);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please check the whole bids for other cases as well

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the requested changes were about asserting the bidder bid object in other tests where it's applicable in the same way you did it in the makeBidsShouldReturnBannerBidForRichMediaType

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you're right. I just wanted to shorten the tests because they have 500 lines of code and check only a few cases instead of all of them.

@CTMBNara CTMBNara merged commit edc2453 into master Nov 18, 2025
8 checks passed
@CTMBNara CTMBNara deleted the New-Adapter-Nativery branch November 18, 2025 10:47
riteshghodrao pushed a commit to riteshghodrao/prebid-server-java that referenced this pull request Apr 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Port PR from PBS-Go: New Adapter: Nativery

3 participants