opensearch-project / opensearch-project/opensearch-java
gRPC transport: typed search silently deserializes hits as Object.class, causing ClassCastException on source access
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 165
- Forks
- 250
- Avg merge
- 1d 18h
- Merged PRs (30d)
- 26
Description
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) callsjsonpMapper.deserialize(parser, tDocumentClass)— withObject.classthis producesJsonData, 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
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. - Because generics are erased,
(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._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 toSearchResponseConverter.fromProto, or - if typed search is not yet supported over gRPC, restrict
isEndpointSupported()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 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 theEndpointargument before callingperformSearch.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Trace performRequest() and performSearch(SearchRequest) in java-client-grpc/src/main/java/org/opensearch/client/transport/grpc/GrpcTransport.java, then inspect SearchResponseConverter.deserializeSource() in translation/SearchResponseConverter. Compare the gRPC path with SearchRequest._ENDPOINT and its typed response deserializer. Done means typed searches materialize hit sources as the requested document type, or unsupported typed searches are prevented from failing late.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- grpc, java
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100