Skip to content

gRPC transport: typed search silently deserializes hits as Object.class, causing ClassCastException on source access #2123

Description

@krishna3554

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:

  1. 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.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions