From 2b5e99432bb7ebec92cfb19087a0b9e8fab6c562 Mon Sep 17 00:00:00 2001 From: Nagendra Mohan Date: Tue, 11 Aug 2026 10:33:51 +0530 Subject: [PATCH 1/2] fix: Prevent InvalidTypeIdException when Prometheus metric has 'type' label Add @JsonTypeInfo(use = JsonTypeInfo.Id.NONE) on PrometheusResult to override the parent DataSourceResult interface's polymorphic type handling. This prevents Jackson from interpreting a metric label named 'type' as the polymorphic type discriminator, which caused InvalidTypeIdException during deserialization. The type dispatch is already handled explicitly via switch statement in ExecuteDirectQueryActionResponse, so polymorphic type annotations are not needed on the concrete class. Resolves opensearch-project#5684 Signed-off-by: Nagendra Mohan --- .../model/datasource/PrometheusResult.java | 2 ++ .../ExecuteDirectQueryActionResponseTest.java | 26 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/direct-query/src/main/java/org/opensearch/sql/directquery/transport/model/datasource/PrometheusResult.java b/direct-query/src/main/java/org/opensearch/sql/directquery/transport/model/datasource/PrometheusResult.java index 09ff30598d8..ab8c67c6a32 100644 --- a/direct-query/src/main/java/org/opensearch/sql/directquery/transport/model/datasource/PrometheusResult.java +++ b/direct-query/src/main/java/org/opensearch/sql/directquery/transport/model/datasource/PrometheusResult.java @@ -7,6 +7,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; import java.util.List; import java.util.Map; @@ -22,6 +23,7 @@ @Getter @Setter @JsonTypeName("prometheus") +@JsonTypeInfo(use = JsonTypeInfo.Id.NONE) @JsonIgnoreProperties(ignoreUnknown = true) public class PrometheusResult implements DataSourceResult { diff --git a/direct-query/src/test/java/org/opensearch/sql/directquery/transport/model/ExecuteDirectQueryActionResponseTest.java b/direct-query/src/test/java/org/opensearch/sql/directquery/transport/model/ExecuteDirectQueryActionResponseTest.java index 9f4ab5fa9e1..e19bc1a143e 100644 --- a/direct-query/src/test/java/org/opensearch/sql/directquery/transport/model/ExecuteDirectQueryActionResponseTest.java +++ b/direct-query/src/test/java/org/opensearch/sql/directquery/transport/model/ExecuteDirectQueryActionResponseTest.java @@ -287,4 +287,30 @@ public void testConstructorWithPrometheusError() throws IOException { PrometheusResult result = (PrometheusResult) response.getResults().get(dataSourceName); assertNotNull(result); } + + @Test + public void testPrometheusResultWithTypeLabelInMetric() throws IOException { + // Regression test: metrics containing a label named "type" should not cause + // InvalidTypeIdException during deserialization (GitHub #5684) + String queryId = "query-type-label"; + String sessionId = "session-type-label"; + String rawResult = + "{\"resultType\":\"vector\",\"result\":[{\"metric\":" + + "{\"__name__\":\"cpu_usage\",\"type\":\"gauge\",\"instance\":\"localhost:9090\"}," + + "\"value\":[1625000000,\"0.5\"]}]}"; + String dataSourceName = "prom-with-type-label"; + String dataSourceType = "prometheus"; + + ExecuteDirectQueryActionResponse response = + new ExecuteDirectQueryActionResponse( + queryId, rawResult, sessionId, dataSourceName, dataSourceType); + + assertEquals(queryId, response.getQueryId()); + assertEquals(1, response.getResults().size()); + assertInstanceOf(PrometheusResult.class, response.getResults().get(dataSourceName)); + PrometheusResult result = (PrometheusResult) response.getResults().get(dataSourceName); + assertNotNull(result.getResult()); + assertEquals(1, result.getResult().size()); + assertEquals("gauge", result.getResult().get(0).getMetric().get("type")); + } } \ No newline at end of file From 08faca347eceb43030adc57d419f8463584c4f85 Mon Sep 17 00:00:00 2001 From: Nagendra Mohan Date: Wed, 26 Aug 2026 17:02:56 +0530 Subject: [PATCH 2/2] Remove redundant Jackson type polymorphism for DataSourceResult The direct-query transport already carries the concrete result type as a separate 'dataSourceType' string and dispatches on it via a switch in both writeTo() and the StreamInput constructor. The Jackson polymorphism on DataSourceResult (@JsonTypeInfo/@JsonSubTypes) was therefore never exercised through the interface, and the earlier @JsonTypeInfo(Id.NONE) workaround plus the addTypeFieldToJson()/contains("\"type\":") guard existed only to satisfy that unused machinery -- which is what let a Prometheus metric label named 'type' break deserialization (#5684). Remove the polymorphism scaffolding entirely: drop the discriminator annotations, read PrometheusResult directly, and delete the type injection. Add a writeTo()->StreamInput round-trip test that carries a metric label named 'type' to cover the transport path. Note: this changes the serialized shape (no root 'type' in the result JSON). Dispatch is unaffected (the separate dataSourceType string), and the API is @opensearch.experimental, so the only impact is a bounded, one-directional mixed-version window during a rolling upgrade. Signed-off-by: Nagendra Mohan --- .../ExecuteDirectQueryActionResponse.java | 21 +--------- .../model/datasource/DataSourceResult.java | 9 ++--- .../model/datasource/PrometheusResult.java | 4 -- .../ExecuteDirectQueryActionResponseTest.java | 38 +++++++++++++++++++ 4 files changed, 43 insertions(+), 29 deletions(-) diff --git a/direct-query/src/main/java/org/opensearch/sql/directquery/transport/model/ExecuteDirectQueryActionResponse.java b/direct-query/src/main/java/org/opensearch/sql/directquery/transport/model/ExecuteDirectQueryActionResponse.java index 0b573e162fc..e5c99c53493 100644 --- a/direct-query/src/main/java/org/opensearch/sql/directquery/transport/model/ExecuteDirectQueryActionResponse.java +++ b/direct-query/src/main/java/org/opensearch/sql/directquery/transport/model/ExecuteDirectQueryActionResponse.java @@ -131,19 +131,11 @@ private Map parseResult( Map parsedResults = new HashMap<>(); try { - // Add type to JSON if it doesn't already have it - final String resultWithType; - if (!rawResult.contains("\"type\":")) { - resultWithType = addTypeFieldToJson(rawResult, dataSourceType); - } else { - resultWithType = rawResult; - } - DataSourceResult result; // Parse based on the determined data source type switch (dataSourceType.toLowerCase()) { case "prometheus": - result = OBJECT_MAPPER.readValue(resultWithType, PrometheusResult.class); + result = OBJECT_MAPPER.readValue(rawResult, PrometheusResult.class); break; // Add cases for other data source types as they're implemented default: @@ -163,15 +155,4 @@ private Map parseResult( return parsedResults; } - - /** - * Adds a type field to the JSON string for proper polymorphic deserialization. - * - * @param rawJson The raw JSON string without a type field - * @param type The type to add - * @return Modified JSON string with type field - */ - private String addTypeFieldToJson(String rawJson, String type) { - return rawJson.replaceFirst("\\{", "{\"type\":\"" + type + "\","); - } } diff --git a/direct-query/src/main/java/org/opensearch/sql/directquery/transport/model/datasource/DataSourceResult.java b/direct-query/src/main/java/org/opensearch/sql/directquery/transport/model/datasource/DataSourceResult.java index b8714623807..6ed77dcf705 100644 --- a/direct-query/src/main/java/org/opensearch/sql/directquery/transport/model/datasource/DataSourceResult.java +++ b/direct-query/src/main/java/org/opensearch/sql/directquery/transport/model/datasource/DataSourceResult.java @@ -5,15 +5,14 @@ package org.opensearch.sql.directquery.transport.model.datasource; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; - /** * * @opensearch.experimental * * Interface for results from various data sources. + * + *

Concrete result types are dispatched by the {@code dataSourceType} string carried alongside + * the serialized payload in the transport protocol (see {@code ExecuteDirectQueryActionResponse}), + * so no in-JSON Jackson type discriminator is used here. */ -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") -@JsonSubTypes({@JsonSubTypes.Type(value = PrometheusResult.class, name = "prometheus")}) public interface DataSourceResult {} diff --git a/direct-query/src/main/java/org/opensearch/sql/directquery/transport/model/datasource/PrometheusResult.java b/direct-query/src/main/java/org/opensearch/sql/directquery/transport/model/datasource/PrometheusResult.java index ab8c67c6a32..ba0abd53cc8 100644 --- a/direct-query/src/main/java/org/opensearch/sql/directquery/transport/model/datasource/PrometheusResult.java +++ b/direct-query/src/main/java/org/opensearch/sql/directquery/transport/model/datasource/PrometheusResult.java @@ -7,8 +7,6 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.annotation.JsonTypeName; import java.util.List; import java.util.Map; import lombok.Getter; @@ -22,8 +20,6 @@ */ @Getter @Setter -@JsonTypeName("prometheus") -@JsonTypeInfo(use = JsonTypeInfo.Id.NONE) @JsonIgnoreProperties(ignoreUnknown = true) public class PrometheusResult implements DataSourceResult { diff --git a/direct-query/src/test/java/org/opensearch/sql/directquery/transport/model/ExecuteDirectQueryActionResponseTest.java b/direct-query/src/test/java/org/opensearch/sql/directquery/transport/model/ExecuteDirectQueryActionResponseTest.java index e19bc1a143e..68ae64d40d6 100644 --- a/direct-query/src/test/java/org/opensearch/sql/directquery/transport/model/ExecuteDirectQueryActionResponseTest.java +++ b/direct-query/src/test/java/org/opensearch/sql/directquery/transport/model/ExecuteDirectQueryActionResponseTest.java @@ -15,6 +15,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.junit.jupiter.api.Test; import org.opensearch.core.common.io.stream.InputStreamStreamInput; @@ -147,6 +148,43 @@ public void testStreamSerializationWithPrometheusResult() throws IOException { assertInstanceOf(PrometheusResult.class, deserializedResponse.getResults().get("prom-ds-1")); } + @Test + public void testStreamSerializationPreservesTypeMetricLabel() throws IOException { + // Regression for #5684: a metric label literally named "type" must survive the + // writeTo() -> StreamInput round-trip (the transport path), not only parseResult(). + Map results = new HashMap<>(); + PrometheusResult prometheusResult = new PrometheusResult(); + prometheusResult.setResultType("vector"); + PrometheusResult.PrometheusResultItem item = new PrometheusResult.PrometheusResultItem(); + Map metric = new HashMap<>(); + metric.put("type", "counter"); + metric.put("__name__", "http_requests_total"); + item.setMetric(metric); + prometheusResult.setResult(List.of(item)); + results.put("prom-ds-1", prometheusResult); + + ExecuteDirectQueryActionResponse response = + new ExecuteDirectQueryActionResponse("query-type-label", results, "session-type-label"); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + StreamOutput streamOutput = new OutputStreamStreamOutput(outputStream); + response.writeTo(streamOutput); + streamOutput.close(); + + StreamInput streamInput = + new InputStreamStreamInput(new ByteArrayInputStream(outputStream.toByteArray())); + ExecuteDirectQueryActionResponse deserialized = + new ExecuteDirectQueryActionResponse(streamInput); + streamInput.close(); + + PrometheusResult out = (PrometheusResult) deserialized.getResults().get("prom-ds-1"); + assertInstanceOf(PrometheusResult.class, out); + assertEquals("vector", out.getResultType()); + assertEquals(1, out.getResult().size()); + assertEquals("counter", out.getResult().get(0).getMetric().get("type")); + assertEquals("http_requests_total", out.getResult().get(0).getMetric().get("__name__")); + } + @Test public void testStreamSerializationWithNullSessionId() throws IOException { String queryId = "query-no-session";