Summary
GrpcTransport.performSearch() discards the caller's document type entirely: it always deserializes hit _source values with Object.class and then unchecked-casts the result to SearchResponse<TDocument>. Unlike the REST transport - where SearchRequest._ENDPOINT instances carry a response deserializer bound to the concrete document class passed to client.search(request, MyType.class) - the gRPC path returns hits whose sources are generic JsonData/Map values. Any typed access to hit.source() fails at runtime with ClassCastException.
Static-analysis finding against current master; not executed here.
Location
- File:
java-client-grpc/src/main/java/org/opensearch/client/transport/grpc/GrpcTransport.java
- Function:
performSearch(SearchRequest request) (~lines 275-297):
// Convert response — use Object.class as default; the actual deserialization
// is handled by the endpoint's response deserializer in the transport layer
return (SearchResponse<TDocument>) ...SearchResponseConverter.fromProto(
protoResponse,
jsonpMapper,
(Class<TDocument>) Object.class
);
- Materialization point:
translation/SearchResponseConverter.deserializeSource() (~line 168-172) calls jsonpMapper.deserialize(parser, tDocumentClass) — with Object.class this produces JsonData, and nothing re-deserializes afterwards.
Problem
Two claims in the code do not hold together:
- The comment states "the actual deserialization is handled by the endpoint's response deserializer in the transport layer". But
performSearch receives only the SearchRequest; the Endpoint (which is exactly what carries the typed response deserializer) is dropped in performRequest() at ~line 146 (performSearch((SearchRequest) request)). No later stage exists that would convert the already-materialized generic hits into TDocument.
- Because generics are erased,
(SearchResponse<TDocument>) succeeds silently; the type error surfaces later as a ClassCastException inside user code iterating response.hits().hits() and calling methods on source().
The REST transport honors the typed contract because SearchRequest._ENDPOINT used by OpenSearchClient.search(req, clazz) embeds clazz in its response parser. The gRPC branch bypasses it.
Trigger / Reproduction
Based on static analysis; no runtime run performed:
OpenSearchTransport grpcTransport = GrpcTransport.builder(...).build();
OpenSearchClient client = new OpenSearchClient(grpcTransport);
SearchResponse<Product> resp = client.search(s -> s.index("products")
.query(q -> q.matchAll(m -> m)), Product.class);
for (Hit<Product> hit : resp.hits().hits()) {
Product p = hit.source(); // compiles; actually a JsonData
p.getName(); // ClassCastException here
}
Expected Behavior
Either:
- extract the document class from the endpoint's response deserializer (or from the
SearchRequest's typed deserializer reference) and pass it to SearchResponseConverter.fromProto, or
- if typed search is not yet supported over gRPC, restrict
isEndpointSupported() so typed searches fall back to REST under HybridTransport, and/or document the limitation loudly instead of failing late with CCE.
Actual Behavior
Silent success with wrongly-typed contents; failure deferred to first element access in user code.
Impact
Every gRPC-transport user of the strongly-typed search API (the primary usage pattern shown in the client docs) gets runtime ClassCastExceptions instead of mapped documents. This makes the new java-client-grpc module unusable for typed search workloads without users knowing why, since compile-time types look correct.
Suggested Direction
Thread the document class through: e.g. have performRequest detect SearchRequest._ENDPOINT's responseDeserializer (opensearch-java endpoints expose their JsonpDeserializer) or add an overload of performSearch(SearchRequest, Endpoint) that pulls tDocumentClass from the deserializer when it is a ObjectDeserializer<TDocument>-style instance, defaulting to JsonData with a logged warning otherwise.
Evidence
- Hard-coded
Object.class + unchecked cast quoted above.
SearchResponseConverter.fromProto/deserializeSource: single-pass materialization using the passed class; no deferred conversion exists.
performRequest() routes solely on request identity and drops the Endpoint argument before calling performSearch.
Summary
GrpcTransport.performSearch()discards the caller's document type entirely: it always deserializes hit_sourcevalues withObject.classand then unchecked-casts the result toSearchResponse<TDocument>. Unlike the REST transport - whereSearchRequest._ENDPOINTinstances carry a response deserializer bound to the concrete document class passed toclient.search(request, MyType.class)- the gRPC path returns hits whose sources are genericJsonData/Map values. Any typed access tohit.source()fails at runtime withClassCastException.Static-analysis finding against current
master; not executed here.Location
java-client-grpc/src/main/java/org/opensearch/client/transport/grpc/GrpcTransport.javaperformSearch(SearchRequest request)(~lines 275-297):translation/SearchResponseConverter.deserializeSource()(~line 168-172) callsjsonpMapper.deserialize(parser, tDocumentClass)— withObject.classthis producesJsonData, and nothing re-deserializes afterwards.Problem
Two claims in the code do not hold together:
performSearchreceives only theSearchRequest; theEndpoint(which is exactly what carries the typed response deserializer) is dropped inperformRequest()at ~line 146 (performSearch((SearchRequest) request)). No later stage exists that would convert the already-materialized generic hits intoTDocument.(SearchResponse<TDocument>)succeeds silently; the type error surfaces later as aClassCastExceptioninside user code iteratingresponse.hits().hits()and calling methods onsource().The REST transport honors the typed contract because
SearchRequest._ENDPOINTused byOpenSearchClient.search(req, clazz)embedsclazzin its response parser. The gRPC branch bypasses it.Trigger / Reproduction
Based on static analysis; no runtime run performed:
Expected Behavior
Either:
SearchRequest's typed deserializer reference) and pass it toSearchResponseConverter.fromProto, orisEndpointSupported()so typed searches fall back to REST underHybridTransport, and/or document the limitation loudly instead of failing late with CCE.Actual Behavior
Silent success with wrongly-typed contents; failure deferred to first element access in user code.
Impact
Every gRPC-transport user of the strongly-typed search API (the primary usage pattern shown in the client docs) gets runtime
ClassCastExceptions instead of mapped documents. This makes the newjava-client-grpcmodule unusable for typed search workloads without users knowing why, since compile-time types look correct.Suggested Direction
Thread the document class through: e.g. have
performRequestdetectSearchRequest._ENDPOINT'sresponseDeserializer(opensearch-java endpoints expose theirJsonpDeserializer) or add an overload ofperformSearch(SearchRequest, Endpoint)that pullstDocumentClassfrom the deserializer when it is aObjectDeserializer<TDocument>-style instance, defaulting toJsonDatawith a logged warning otherwise.Evidence
Object.class+ unchecked cast quoted above.SearchResponseConverter.fromProto/deserializeSource: single-pass materialization using the passed class; no deferred conversion exists.performRequest()routes solely on request identity and drops theEndpointargument before callingperformSearch.