From f139f9641da47f874fee5b9da63c0cffbc960b3e Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Thu, 15 Oct 2026 18:43:10 -0400 Subject: [PATCH 1/5] Support for searching list items --- .../java/org/brapi/client/v2/BrAPIClient.java | 3 + .../client/v2/modules/core/ListsApi.java | 67 +++++++++++++ brapi-java-model/pom.xml | 5 + .../brapi/v2/model/core/BrAPIListItem.java | 85 ++++++++++++++++ .../model/core/BrAPIListItemWithFullItem.java | 71 +++++++++++++ .../response/BrAPIListItemsListResponse.java | 99 +++++++++++++++++++ .../BrAPIListItemsListResponseResult.java | 74 ++++++++++++++ pom.xml | 19 +--- 8 files changed, 408 insertions(+), 15 deletions(-) create mode 100644 brapi-java-model/src/main/java/org/brapi/v2/model/core/BrAPIListItem.java create mode 100644 brapi-java-model/src/main/java/org/brapi/v2/model/core/BrAPIListItemWithFullItem.java create mode 100644 brapi-java-model/src/main/java/org/brapi/v2/model/core/response/BrAPIListItemsListResponse.java create mode 100644 brapi-java-model/src/main/java/org/brapi/v2/model/core/response/BrAPIListItemsListResponseResult.java diff --git a/brapi-java-client/src/main/java/org/brapi/client/v2/BrAPIClient.java b/brapi-java-client/src/main/java/org/brapi/client/v2/BrAPIClient.java index e1f29491..432c7142 100644 --- a/brapi-java-client/src/main/java/org/brapi/client/v2/BrAPIClient.java +++ b/brapi-java-client/src/main/java/org/brapi/client/v2/BrAPIClient.java @@ -12,6 +12,7 @@ package org.brapi.client.v2; +import com.fasterxml.jackson.databind.JsonNode; import com.google.gson.*; import com.google.gson.reflect.TypeToken; import okhttp3.*; @@ -674,6 +675,8 @@ public RequestBody serialize(Object obj, String contentType) throws ApiException } else if (obj instanceof File) { // File body parameter support. return RequestBody.create(MediaType.parse(contentType), (File) obj); + } else if (obj instanceof JsonNode) { + return RequestBody.create(MediaType.parse(contentType), obj.toString()); } else if (isJsonMime(contentType)) { String content; if (obj != null) { diff --git a/brapi-java-client/src/main/java/org/brapi/client/v2/modules/core/ListsApi.java b/brapi-java-client/src/main/java/org/brapi/client/v2/modules/core/ListsApi.java index 93ec1402..8782ac14 100644 --- a/brapi-java-client/src/main/java/org/brapi/client/v2/modules/core/ListsApi.java +++ b/brapi-java-client/src/main/java/org/brapi/client/v2/modules/core/ListsApi.java @@ -12,6 +12,7 @@ package org.brapi.client.v2.modules.core; +import com.fasterxml.jackson.databind.JsonNode; import com.google.gson.reflect.TypeToken; import okhttp3.Call; @@ -29,7 +30,10 @@ import org.brapi.client.v2.model.exceptions.ApiException; import org.brapi.client.v2.model.queryParams.core.ListQueryParams; import org.brapi.v2.model.BrAPIAcceptedSearchResponse; +import org.brapi.v2.model.BrAPISearchRequestParametersPaging; +import org.brapi.v2.model.core.BrAPIListItemWithFullItem; import org.brapi.v2.model.core.request.BrAPIListNewRequest; +import org.brapi.v2.model.core.response.BrAPIListItemsListResponse; import org.brapi.v2.model.core.response.BrAPIListResponse; import org.brapi.v2.model.core.request.BrAPIListSearchRequest; import org.brapi.v2.model.core.response.BrAPIListsListResponse; @@ -537,6 +541,45 @@ private Call searchListsPostCall(BrAPIListSearchRequest body) throws ApiExceptio return apiClient.buildCall(localVarPath, "POST", localVarQueryParams, localVarCollectionQueryParams, localVarPostBody, localVarHeaderParams, localVarFormParams, localVarAuthNames); } + /** + * Build call for searchListsPost + * @param body (optional) + * @return Call to execute + * @throws ApiException If fail to serialize the request body object + */ + private Call searchListItemsPostCall(String listDbId, JsonNode body) throws ApiException { + if(body == null) { + throw new IllegalArgumentException("body cannot be null"); + } + Object localVarPostBody = body; + + // create path and map variables + String localVarPath = "/search/lists/{listDbId}" + .replaceAll("\\{" + "listDbId" + "\\}", apiClient.escapeString(listDbId)) + "/items"; + + Map localVarQueryParams = new HashMap<>(); + Map localVarCollectionQueryParams = new HashMap<>(); + + Map localVarHeaderParams = new HashMap(); + + Map localVarFormParams = new HashMap(); + + final String[] localVarAccepts = { + "application/json" + }; + final String localVarAccept = apiClient.selectHeaderAccept(localVarAccepts); + if (localVarAccept != null) localVarHeaderParams.put("Accept", localVarAccept); + + final String[] localVarContentTypes = { + "application/json" + }; + final String localVarContentType = apiClient.selectHeaderContentType(localVarContentTypes); + localVarHeaderParams.put("Content-Type", localVarContentType); + + String[] localVarAuthNames = new String[] { "AuthorizationToken" }; + return apiClient.buildCall(localVarPath, "POST", localVarQueryParams, localVarCollectionQueryParams, localVarPostBody, localVarHeaderParams, localVarFormParams, localVarAuthNames); + } + /** * Submit a search request for Lists * Advanced searching for the list resource. See Search Services for additional implementation details. @@ -642,4 +685,28 @@ public Call searchListsSearchResultsDbIdGetAsync(String searchResultsDbId, Integ apiClient.executeAsync(call, localVarReturnType, callback); return call; } + + /** + * Submit a search request for ListItems + * Unlike other endpoints, this endpoint is not strongly typed to the search request, and is detected based off of the submitted listDbId. + * Because of this, create the SearchRequest of the entity that belongs to the list, for example. Like a BrAPIGermplasmSearchRequest. + * Then, to conform to the detection this endpoint provides for the specific list, convert the object to JsonNode. + * @param listDbId (required) + * @param body (optional) + * @param itemClass (required) This is the BrAPI model class that you want each item to represent. + * @return ApiResponse<ListsListResponse> + * @throws ApiException If fail to call the API, e.g. server error or cannot deserialize the response body + */ + public ApiResponse> searchListItemsByList(String listDbId, JsonNode body, Class itemClass) throws ApiException { + Call call = searchListItemsPostCall(listDbId, body); + + // This will populate the parameterized class throughout the BrAPIListItemsListResponse chain, all the way down + // to where the items live in BrAPIListItemsListResponseResult.data + Type listResponseType = TypeToken.getParameterized( + BrAPIListItemsListResponse.class, + itemClass + ).getType(); + + return apiClient.execute(call, listResponseType); + } } diff --git a/brapi-java-model/pom.xml b/brapi-java-model/pom.xml index a682c4a0..0fd7cd76 100644 --- a/brapi-java-model/pom.xml +++ b/brapi-java-model/pom.xml @@ -40,6 +40,11 @@ jackson-annotations ${jackson.annotations.version} + + com.fasterxml.jackson.core + jackson-databind + ${jackson.annotations.version} + javax.validation validation-api diff --git a/brapi-java-model/src/main/java/org/brapi/v2/model/core/BrAPIListItem.java b/brapi-java-model/src/main/java/org/brapi/v2/model/core/BrAPIListItem.java new file mode 100644 index 00000000..8cc02e0a --- /dev/null +++ b/brapi-java-model/src/main/java/org/brapi/v2/model/core/BrAPIListItem.java @@ -0,0 +1,85 @@ +package org.brapi.v2.model.core; + +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * ListItem + */ +public class BrAPIListItem { + @JsonProperty("listItemDbId") + private String listItemDbId = null; + + @JsonProperty("position") + private Integer position = null; + + // This represents the actual value stored in the list_item table + @JsonProperty("item") + private String item = null; + + + public String getListItemDbId() { + return listItemDbId; + } + + public void setListItemDbId(String listItemDbId) { + this.listItemDbId = listItemDbId; + } + + public Integer getPosition() { + return position; + } + + public void setPosition(Integer position) { + this.position = position; + } + + public String getItem() { + return item; + } + + public void setItem(String item) { + this.item = item; + } + + @Override + public boolean equals(java.lang.Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + BrAPIListItem ListItem = (BrAPIListItem) o; + return Objects.equals(this.listItemDbId, ListItem.listItemDbId) && + super.equals(o); + } + + @Override + public int hashCode() { + return Objects.hash(listItemDbId, super.hashCode()); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ListItem {\n"); + sb.append(" ").append(toIndentedString(super.toString())).append("\n"); + sb.append(" listDbId: ").append(toIndentedString(listItemDbId)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(java.lang.Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} + + diff --git a/brapi-java-model/src/main/java/org/brapi/v2/model/core/BrAPIListItemWithFullItem.java b/brapi-java-model/src/main/java/org/brapi/v2/model/core/BrAPIListItemWithFullItem.java new file mode 100644 index 00000000..68717424 --- /dev/null +++ b/brapi-java-model/src/main/java/org/brapi/v2/model/core/BrAPIListItemWithFullItem.java @@ -0,0 +1,71 @@ +package org.brapi.v2.model.core; + +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * ListItemWithFullItem + */ +public class BrAPIListItemWithFullItem { + @JsonProperty("listItem") + private BrAPIListItem listItem = null; + + @JsonProperty("item") + private T item = null; + + public BrAPIListItem getListItem() { + return listItem; + } + + public void setListItem(BrAPIListItem listItem) { + this.listItem = listItem; + } + + public T getItem() { + return item; + } + + public void setItem(T item) { + this.item = item; + } + + @Override + public boolean equals(java.lang.Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + BrAPIListItemWithFullItem listItemWithFullItem = (BrAPIListItemWithFullItem) o; + return Objects.equals(this.listItem, listItemWithFullItem.listItem) && + super.equals(o); + } + + @Override + public int hashCode() { + return Objects.hash(listItem, super.hashCode()); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ListItemWithFullItem {\n"); + sb.append(" ").append(toIndentedString(super.toString())).append("\n"); + sb.append(" listItem: ").append(toIndentedString(listItem)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(java.lang.Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} + diff --git a/brapi-java-model/src/main/java/org/brapi/v2/model/core/response/BrAPIListItemsListResponse.java b/brapi-java-model/src/main/java/org/brapi/v2/model/core/response/BrAPIListItemsListResponse.java new file mode 100644 index 00000000..19bcdddc --- /dev/null +++ b/brapi-java-model/src/main/java/org/brapi/v2/model/core/response/BrAPIListItemsListResponse.java @@ -0,0 +1,99 @@ +package org.brapi.v2.model.core.response; + +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.brapi.v2.model.BrAPIContext; +import org.brapi.v2.model.BrAPIMetadata; +import org.brapi.v2.model.BrAPIResponse; + +/** + * ListItemsListResponse + */ +public class BrAPIListItemsListResponse implements BrAPIResponse> { + @JsonProperty("@context") + private BrAPIContext _atContext = null; + + @JsonProperty("metadata") + private BrAPIMetadata metadata = null; + + @JsonProperty("result") + private BrAPIListItemsListResponseResult result = null; + + public BrAPIListItemsListResponse() { + this._atContext = new BrAPIContext(); + this._atContext.add("context"); + } + + + public void set_atContext(BrAPIContext _atContext) { + this._atContext = _atContext; + } + public BrAPIListItemsListResponse metadata(BrAPIMetadata metadata) { + this.metadata = metadata; + return this; + } + + public BrAPIMetadata getMetadata() { + return metadata; + } + + public void setMetadata(BrAPIMetadata metadata) { + this.metadata = metadata; + } + + public BrAPIListItemsListResponse result(BrAPIListItemsListResponseResult result) { + this.result = result; + return this; + } + + + public BrAPIListItemsListResponseResult getResult() { + return result; + } + + public void setResult(BrAPIListItemsListResponseResult result) { + this.result = result; + } + + @Override + public boolean equals(java.lang.Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + BrAPIListItemsListResponse listItemsListResponse = (BrAPIListItemsListResponse) o; + return Objects.equals(this._atContext, listItemsListResponse._atContext) + && Objects.equals(this.metadata, listItemsListResponse.metadata) + && Objects.equals(this.result, listItemsListResponse.result); + } + + @Override + public int hashCode() { + return Objects.hash(_atContext, metadata, result); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ListItemsListResponse {\n"); + + sb.append(" _atContext: ").append(toIndentedString(_atContext)).append("\n"); + sb.append(" metadata: ").append(toIndentedString(metadata)).append("\n"); + sb.append(" result: ").append(toIndentedString(result)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(java.lang.Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} diff --git a/brapi-java-model/src/main/java/org/brapi/v2/model/core/response/BrAPIListItemsListResponseResult.java b/brapi-java-model/src/main/java/org/brapi/v2/model/core/response/BrAPIListItemsListResponseResult.java new file mode 100644 index 00000000..d5d4221d --- /dev/null +++ b/brapi-java-model/src/main/java/org/brapi/v2/model/core/response/BrAPIListItemsListResponseResult.java @@ -0,0 +1,74 @@ +package org.brapi.v2.model.core.response; + +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.brapi.v2.model.BrAPIResponseResult; +import org.brapi.v2.model.core.BrAPIListItemWithFullItem; + +import java.util.ArrayList; +import java.util.List; + +/** + * ListItemsListResponseResult + */ +public class BrAPIListItemsListResponseResult implements BrAPIResponseResult> { + @JsonProperty("data") + private List> data = new ArrayList<>(); + + public BrAPIListItemsListResponseResult data(List> data) { + this.data = data; + return this; + } + + public BrAPIListItemsListResponseResult addDataItem(BrAPIListItemWithFullItem dataItem) { + this.data.add(dataItem); + return this; + } + public List> getData() { + return data; + } + + public void setData(List> data) { + this.data = data; + } + + + @Override + public boolean equals(java.lang.Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + BrAPIListItemsListResponseResult listItemsListResponseResult = (BrAPIListItemsListResponseResult) o; + return Objects.equals(this.data, listItemsListResponseResult.data); + } + + @Override + public int hashCode() { + return Objects.hash(data); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ListItemsListResponseResult {\n"); + + sb.append(" data: ").append(toIndentedString(data)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(java.lang.Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} + diff --git a/pom.xml b/pom.xml index 2c313c6f..c4ee9af5 100644 --- a/pom.xml +++ b/pom.xml @@ -130,28 +130,17 @@ - ossrh - https://s01.oss.sonatype.org/content/repositories/snapshots/ + github + https://maven.pkg.github.com/Breeding-Insight/brapi - ossrh - https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ + github + https://maven.pkg.github.com/Breeding-Insight/brapi - - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.7 - true - - ossrh - https://s01.oss.sonatype.org/ - true - - maven-compiler-plugin From 476c450511f16a70cac79870233d1ae0945af924 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Fri, 16 Oct 2026 10:55:42 -0400 Subject: [PATCH 2/5] Undo package deploying changes to pom --- pom.xml | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index c4ee9af5..2c313c6f 100644 --- a/pom.xml +++ b/pom.xml @@ -130,17 +130,28 @@ - github - https://maven.pkg.github.com/Breeding-Insight/brapi + ossrh + https://s01.oss.sonatype.org/content/repositories/snapshots/ - github - https://maven.pkg.github.com/Breeding-Insight/brapi + ossrh + https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.7 + true + + ossrh + https://s01.oss.sonatype.org/ + true + + maven-compiler-plugin From e841fd68d587c93ecce947d369189f5c14af884b Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Fri, 16 Oct 2026 14:55:52 -0400 Subject: [PATCH 3/5] Force type parametrization serialization on the client --- .../java/org/brapi/client/v2/BrAPIClient.java | 38 +++++++++++++++- .../brapi/client/v2/ResponseDeserializer.java | 11 +++++ .../client/v2/modules/core/ListsApi.java | 45 +++++++++++++++---- .../response/BrAPIListItemsListResponse.java | 12 +++++ 4 files changed, 97 insertions(+), 9 deletions(-) create mode 100644 brapi-java-client/src/main/java/org/brapi/client/v2/ResponseDeserializer.java diff --git a/brapi-java-client/src/main/java/org/brapi/client/v2/BrAPIClient.java b/brapi-java-client/src/main/java/org/brapi/client/v2/BrAPIClient.java index 432c7142..6d3dd7e9 100644 --- a/brapi-java-client/src/main/java/org/brapi/client/v2/BrAPIClient.java +++ b/brapi-java-client/src/main/java/org/brapi/client/v2/BrAPIClient.java @@ -762,7 +762,8 @@ public File prepareDownloadFile(Response response) throws IOException { * @return ApiResponse<T> */ public ApiResponse execute(Call call) throws ApiException { - return execute(call, null); + Type returnType = null; + return execute(call, returnType); } /** @@ -787,6 +788,16 @@ public ApiResponse execute(Call call, Type returnType) throws ApiExceptio } } + public ApiResponse execute(Call call, ResponseDeserializer responseDeserializer) throws ApiException { + try { + Response response = call.execute(); + T data = handleResponse(response, responseDeserializer); + return new ApiResponse(response.code(), response.headers().toMultimap(), data); + } catch (IOException e) { + throw new ApiException(e); + } + } + /** * Execute HTTP call and deserialize the HTTP response body into the given * return type. @@ -911,6 +922,31 @@ public T handleResponse(Response response, Type returnType) throws ApiExcept } } + public T handleResponse(Response response, ResponseDeserializer deserializer) throws ApiException, IOException { + if (response.isSuccessful()) { + if (response.code() == 204) { + // returning null if the returnType is not defined, + // or the status code is 204 (No Content) + if (response.body() != null) { + response.body().close(); + } + return null; + } else { + return deserializer.deserialize(response); + } + } else { + String respBody = null; + if (response.body() != null) { + try { + respBody = response.body().string(); + } catch (IOException e) { + throw new ApiException(response.message(), e, response.code(), response.headers().toMultimap()); + } + } + throw new ApiException(response.message(), response.code(), response.headers().toMultimap(), respBody); + } + } + /** * Build HTTP call with the given options. * diff --git a/brapi-java-client/src/main/java/org/brapi/client/v2/ResponseDeserializer.java b/brapi-java-client/src/main/java/org/brapi/client/v2/ResponseDeserializer.java new file mode 100644 index 00000000..c96a2bbd --- /dev/null +++ b/brapi-java-client/src/main/java/org/brapi/client/v2/ResponseDeserializer.java @@ -0,0 +1,11 @@ +package org.brapi.client.v2; + +import okhttp3.Response; +import org.brapi.client.v2.model.exceptions.ApiException; + +import java.io.IOException; + +@FunctionalInterface +public interface ResponseDeserializer { + T deserialize(Response response) throws ApiException, IOException; +} diff --git a/brapi-java-client/src/main/java/org/brapi/client/v2/modules/core/ListsApi.java b/brapi-java-client/src/main/java/org/brapi/client/v2/modules/core/ListsApi.java index 8782ac14..bb118789 100644 --- a/brapi-java-client/src/main/java/org/brapi/client/v2/modules/core/ListsApi.java +++ b/brapi-java-client/src/main/java/org/brapi/client/v2/modules/core/ListsApi.java @@ -13,15 +13,21 @@ package org.brapi.client.v2.modules.core; import com.fasterxml.jackson.databind.JsonNode; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import com.google.gson.reflect.TypeToken; import okhttp3.Call; + +import java.io.IOException; import java.lang.reflect.Type; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import okhttp3.Response; import org.apache.commons.lang3.tuple.Pair; import org.brapi.client.v2.ApiCallback; import org.brapi.client.v2.BrAPIClient; @@ -30,14 +36,14 @@ import org.brapi.client.v2.model.exceptions.ApiException; import org.brapi.client.v2.model.queryParams.core.ListQueryParams; import org.brapi.v2.model.BrAPIAcceptedSearchResponse; -import org.brapi.v2.model.BrAPISearchRequestParametersPaging; -import org.brapi.v2.model.core.BrAPIListItemWithFullItem; +import org.brapi.v2.model.core.BrAPIListTypes; import org.brapi.v2.model.core.request.BrAPIListNewRequest; import org.brapi.v2.model.core.response.BrAPIListItemsListResponse; import org.brapi.v2.model.core.response.BrAPIListResponse; import org.brapi.v2.model.core.request.BrAPIListSearchRequest; import org.brapi.v2.model.core.response.BrAPIListsListResponse; import org.brapi.v2.model.core.response.BrAPIListsSingleResponse; +import org.brapi.v2.model.germ.BrAPIGermplasm; public class ListsApi { private BrAPIClient apiClient; @@ -58,6 +64,12 @@ public void setApiClient(BrAPIClient apiClient) { this.apiClient = apiClient; } + private static final Map> supportedItemClassByBrAPIListTypes = new HashMap<>(); + + static { + supportedItemClassByBrAPIListTypes.put(BrAPIListTypes.GERMPLASM, BrAPIGermplasm.class); + } + /** * Build call for listsGet * @param queryParams @@ -693,20 +705,37 @@ public Call searchListsSearchResultsDbIdGetAsync(String searchResultsDbId, Integ * Then, to conform to the detection this endpoint provides for the specific list, convert the object to JsonNode. * @param listDbId (required) * @param body (optional) - * @param itemClass (required) This is the BrAPI model class that you want each item to represent. * @return ApiResponse<ListsListResponse> * @throws ApiException If fail to call the API, e.g. server error or cannot deserialize the response body */ - public ApiResponse> searchListItemsByList(String listDbId, JsonNode body, Class itemClass) throws ApiException { + public ApiResponse> searchListItemsByList(String listDbId, JsonNode body) throws ApiException { Call call = searchListItemsPostCall(listDbId, body); - // This will populate the parameterized class throughout the BrAPIListItemsListResponse chain, all the way down - // to where the items live in BrAPIListItemsListResponseResult.data - Type listResponseType = TypeToken.getParameterized( + return apiClient.execute(call, this::deserializeListItemsResponse); + } + + private BrAPIListItemsListResponse deserializeListItemsResponse(Response response) throws ApiException, IOException { + + // Ignore NPE check here, this function is used after a response check has already occurred further downstream + String json = response.body().string(); + + JsonParser parser = new JsonParser(); + + JsonObject root = parser.parse(json).getAsJsonObject(); + + BrAPIListTypes listType = BrAPIListTypes.fromValue(root.getAsJsonPrimitive("resultListType").getAsString()); + + Class itemClass = supportedItemClassByBrAPIListTypes.get(listType); + + if (itemClass == null) { + throw new ApiException("Unsupported list type: " + listType); + } + + Type responseType = TypeToken.getParameterized( BrAPIListItemsListResponse.class, itemClass ).getType(); - return apiClient.execute(call, listResponseType); + return apiClient.getJSON().deserialize(json, responseType); } } diff --git a/brapi-java-model/src/main/java/org/brapi/v2/model/core/response/BrAPIListItemsListResponse.java b/brapi-java-model/src/main/java/org/brapi/v2/model/core/response/BrAPIListItemsListResponse.java index 19bcdddc..f71fa84d 100644 --- a/brapi-java-model/src/main/java/org/brapi/v2/model/core/response/BrAPIListItemsListResponse.java +++ b/brapi-java-model/src/main/java/org/brapi/v2/model/core/response/BrAPIListItemsListResponse.java @@ -5,6 +5,7 @@ import org.brapi.v2.model.BrAPIContext; import org.brapi.v2.model.BrAPIMetadata; import org.brapi.v2.model.BrAPIResponse; +import org.brapi.v2.model.core.BrAPIListTypes; /** * ListItemsListResponse @@ -19,6 +20,9 @@ public class BrAPIListItemsListResponse implements BrAPIResponse result = null; + @JsonProperty("resultListType") + private BrAPIListTypes resultListType = null; + public BrAPIListItemsListResponse() { this._atContext = new BrAPIContext(); this._atContext.add("context"); @@ -55,6 +59,14 @@ public void setResult(BrAPIListItemsListResponseResult result) { this.result = result; } + public BrAPIListTypes getResultListType() { + return resultListType; + } + + public void setResultListType(BrAPIListTypes resultListType) { + this.resultListType = resultListType; + } + @Override public boolean equals(java.lang.Object o) { if (this == o) { From 05f48c98139cbe93c9557c8d9193469c6e15fc50 Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Mon, 19 Oct 2026 17:11:28 -0400 Subject: [PATCH 4/5] Add comment in ListsApi --- .../java/org/brapi/client/v2/modules/core/ListsApi.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/brapi-java-client/src/main/java/org/brapi/client/v2/modules/core/ListsApi.java b/brapi-java-client/src/main/java/org/brapi/client/v2/modules/core/ListsApi.java index bb118789..13b0ed88 100644 --- a/brapi-java-client/src/main/java/org/brapi/client/v2/modules/core/ListsApi.java +++ b/brapi-java-client/src/main/java/org/brapi/client/v2/modules/core/ListsApi.java @@ -701,10 +701,10 @@ public Call searchListsSearchResultsDbIdGetAsync(String searchResultsDbId, Integ /** * Submit a search request for ListItems * Unlike other endpoints, this endpoint is not strongly typed to the search request, and is detected based off of the submitted listDbId. - * Because of this, create the SearchRequest of the entity that belongs to the list, for example. Like a BrAPIGermplasmSearchRequest. - * Then, to conform to the detection this endpoint provides for the specific list, convert the object to JsonNode. + * To accommodate for this, a functional interface is passed to the apiClient to ensure the parameterized is properly initialized to the right BrAPI model object. + * Async and searchDbIds are not currently supported for this endpoint. * @param listDbId (required) - * @param body (optional) + * @param body (required) * @return ApiResponse<ListsListResponse> * @throws ApiException If fail to call the API, e.g. server error or cannot deserialize the response body */ From 7b8d23e44e7ad346db2379544a2b2eebec1c4ded Mon Sep 17 00:00:00 2001 From: Jason Loux Date: Mon, 19 Oct 2026 17:51:15 -0400 Subject: [PATCH 5/5] Add comments --- .../java/org/brapi/client/v2/BrAPIClient.java | 24 +++++++++++++++++++ .../brapi/client/v2/ResponseDeserializer.java | 4 ++++ .../client/v2/modules/core/ListsApi.java | 5 ++++ 3 files changed, 33 insertions(+) diff --git a/brapi-java-client/src/main/java/org/brapi/client/v2/BrAPIClient.java b/brapi-java-client/src/main/java/org/brapi/client/v2/BrAPIClient.java index 6d3dd7e9..f8194e5e 100644 --- a/brapi-java-client/src/main/java/org/brapi/client/v2/BrAPIClient.java +++ b/brapi-java-client/src/main/java/org/brapi/client/v2/BrAPIClient.java @@ -788,6 +788,19 @@ public ApiResponse execute(Call call, Type returnType) throws ApiExceptio } } + /** + * Execute HTTP call and deserialize the HTTP response body into a return type based off of the submitted ResponseDeserializer. + * + * @param call Call + * @param responseDeserializer A function passed through which will be applied to the final response. + * In particular, this function needs to set the return datatype. + * See ListsApi.deserializeListItemsResponse() for usage examples. + * @param The return type corresponding to (same with) returnType + * @return ApiResponse object containing response status, headers and data, + * which is a Java object deserialized from response body and would be + * null when returnType is null. + * @throws ApiException If fail to execute the call + */ public ApiResponse execute(Call call, ResponseDeserializer responseDeserializer) throws ApiException { try { Response response = call.execute(); @@ -922,6 +935,17 @@ public T handleResponse(Response response, Type returnType) throws ApiExcept } } + /** + * Handle the given response, return the deserialized object when the response + * is successful, and apply the additional passed ResponseDeserializer logic. + * + * @param response Response + * @param deserializer A function passed through which will be applied to the final response. + * In particular, this function needs to set the return datatype. + * See ListsApi.deserializeListItemsResponse() for usage examples. * @throws ApiException If the response has a unsuccessful status code or fail + * to deserialize the response body + * @return Type + */ public T handleResponse(Response response, ResponseDeserializer deserializer) throws ApiException, IOException { if (response.isSuccessful()) { if (response.code() == 204) { diff --git a/brapi-java-client/src/main/java/org/brapi/client/v2/ResponseDeserializer.java b/brapi-java-client/src/main/java/org/brapi/client/v2/ResponseDeserializer.java index c96a2bbd..a082d381 100644 --- a/brapi-java-client/src/main/java/org/brapi/client/v2/ResponseDeserializer.java +++ b/brapi-java-client/src/main/java/org/brapi/client/v2/ResponseDeserializer.java @@ -5,6 +5,10 @@ import java.io.IOException; +/** + * Generic interface to apply extra deserialization logic to handle responses in the BrAPIClient. + * @param The response type this response deserializer should apply to. + */ @FunctionalInterface public interface ResponseDeserializer { T deserialize(Response response) throws ApiException, IOException; diff --git a/brapi-java-client/src/main/java/org/brapi/client/v2/modules/core/ListsApi.java b/brapi-java-client/src/main/java/org/brapi/client/v2/modules/core/ListsApi.java index 13b0ed88..b2f5e404 100644 --- a/brapi-java-client/src/main/java/org/brapi/client/v2/modules/core/ListsApi.java +++ b/brapi-java-client/src/main/java/org/brapi/client/v2/modules/core/ListsApi.java @@ -723,19 +723,24 @@ private BrAPIListItemsListResponse deserializeListItemsResponse(Response JsonObject root = parser.parse(json).getAsJsonObject(); + // The prod server response will return with the data type. BrAPIListTypes listType = BrAPIListTypes.fromValue(root.getAsJsonPrimitive("resultListType").getAsString()); + // Get the BrAPI model object that corresponds to the returned data type. Class itemClass = supportedItemClassByBrAPIListTypes.get(listType); if (itemClass == null) { throw new ApiException("Unsupported list type: " + listType); } + // Ensure that BrAPIListItemsListResponse has its parameterized set with the appropriate object, which will + // populate all the way down to BrAPIListItemWithFullItem Type responseType = TypeToken.getParameterized( BrAPIListItemsListResponse.class, itemClass ).getType(); + // Apply deserialization of the response return apiClient.getJSON().deserialize(json, responseType); } }