Skip to content

Fix ClassCastException from immutable map returned by StreamInput.readMap()#968

Open
thecodingshrimp wants to merge 6 commits into
opensearch-project:mainfrom
thecodingshrimp:fix/monitor-uimetadata-immutable-map-cast
Open

Fix ClassCastException from immutable map returned by StreamInput.readMap()#968
thecodingshrimp wants to merge 6 commits into
opensearch-project:mainfrom
thecodingshrimp:fix/monitor-uimetadata-immutable-map-cast

Conversation

@thecodingshrimp

@thecodingshrimp thecodingshrimp commented Jun 9, 2026

Copy link
Copy Markdown

Summary

Fixes #967

StreamInput.readMap() documents that for zero-size maps it might return an immutable map (Collections.emptyMap()). Three deserialization constructors in this repo perform an unchecked cast (suppressWarning) that assumes a MutableMap, causing a ClassCastException at runtime when a monitor with empty uiMetadata, lastRunContext, or queryResults is deserialized over transport.

Exception:

kotlin.collections.EmptyMap cannot be cast to kotlin.collections.MutableMap

Root cause

StreamOutput.writeMap / StreamInput.readMap guarantee only that the result implements java.util.Map. The Javadoc states:

If the returned map contains any entries it will be mutable. If it is empty it might be immutable.

The suppressWarning() helper performs map as MutableMap<String, Any> without a defensive copy, which fails when the deserialized map is Collections.emptyMap().

Identified via opensearch-project/security-analytics#1722TransportIndexDetectorAction passes Map.of() as uiMetadata when constructing a Monitor, which serializes as a zero-size map, readMap() returns Collections.emptyMap(), and the cast fails.

Changes

Replace suppressWarning(sin.readMap()) with sin.readMap()?.toMutableMap() ?: mutableMapOf() at all three callsites and remove the suppressWarning() helper function entirely.

File Change
Monitor.kt uiMetadata deserialization — safe copy
MonitorMetadata.kt lastRunContext deserialization — safe copy
Alert.kt queryResults deserialization — safe copy; remove suppressWarning import

Testing

All existing Monitor-related tests pass (./gradlew test --tests "*Monitor*" → BUILD SUCCESSFUL).

Check List

  • Existing tests pass
  • Commits signed off (DCO)
  • New tests (not added — fix is a defensive copy at deserialization; behaviour is identical for non-empty maps)

Related

…dMap()

StreamInput.readMap() documents that empty maps 'might be immutable'.
Replace all unsafe suppressWarning() unchecked casts and raw readMap() casts
with safe toMutableMap() copies in deserialization constructors across all
affected alerting model classes.

Fixed files:
- Monitor.kt: uiMetadata deserialization
- Alert.kt: queryResults deserialization
- MonitorMetadata.kt: lastRunContext and sourceToQueryIndexMapping
- IndexExecutionContext.kt: lastRunContext, updatedLastRunContext
- DocLevelMonitorFanOutResponse.kt: lastRunContexts; remove suppressWarning helper
- WorkflowRunResult.kt: triggerResults; remove suppressWarning helper

This prevents the kotlin.collections.EmptyMap cannot be cast to
kotlin.collections.MutableMap exception when transport-deserializing
monitors with empty map fields.

Fixes opensearch-project/security-analytics#1715

Signed-off-by: thecodingshrimp <leonard.stutzer@sap.com>
@thecodingshrimp thecodingshrimp force-pushed the fix/monitor-uimetadata-immutable-map-cast branch from f3545a8 to 9511928 Compare June 9, 2026 11:26
@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

(Review updated until commit 504f10c)

Here are some key observations to aid the review process:

🧪 PR contains tests
🔒 No security concerns identified
✅ No TODO sections
🔀 No multiple PR themes
⚡ Recommended focus areas for review

Unsafe Cast

Line 40 casts toMutableMap() result to MutableMap<String, String> without verifying the map's value types. If sin.readMap() returns a map with non-String values (e.g., nested maps or other objects), the cast succeeds at compile time but fails at runtime when code attempts to treat values as String. This occurs when deserializing sourceToQueryIndexMapping from a corrupted or malicious stream.

sourceToQueryIndexMapping = (sin.readMap()?.toMutableMap() ?: mutableMapOf()) as MutableMap<String, String>
Unsafe Cast

Line 38 casts toMutableMap() result to Map<String, ChainedAlertTriggerRunResult> without verifying value types. If sin.readMap() returns a map with incompatible values, the cast succeeds but subsequent access to map values throws ClassCastException. This occurs when deserializing triggerResults from a stream containing unexpected data types.

triggerResults = (sin.readMap()?.toMutableMap() ?: mutableMapOf()) as Map<String, ChainedAlertTriggerRunResult>
Unsafe Cast

Lines 36-37 cast sin.readMap(...) result to Map<String, DocumentLevelTriggerRunResult> without verifying the map is non-null or contains the expected types. If readMap returns null or a map with incompatible values, the cast fails at runtime when the map is accessed. This occurs when deserializing triggerResults from a malformed stream.

triggerResults = (sin.readMap(StreamInput::readString, DocumentLevelTriggerRunResult::readFrom) ?: mapOf())
    as Map<String, DocumentLevelTriggerRunResult>,

@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

Latest suggestions up to e518795

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Unsafe cast to typed trigger results

The cast to Map<String, ChainedAlertTriggerRunResult> is unsafe and will fail at
runtime since readMap() returns a map with generic values, not typed
ChainedAlertTriggerRunResult instances. Use a typed read method or validate entries
before casting.

src/main/kotlin/org/opensearch/commons/alerting/model/WorkflowRunResult.kt [38]

-triggerResults = (sin.readMap()?.toMutableMap() ?: mutableMapOf()) as Map<String, ChainedAlertTriggerRunResult>
+triggerResults = sin.readMap(StreamInput::readString, ChainedAlertTriggerRunResult::readFrom)?.toMutableMap() ?: mutableMapOf()
Suggestion importance[1-10]: 9

__

Why: The cast to Map<String, ChainedAlertTriggerRunResult> is unsafe because readMap() returns untyped values. This will cause ClassCastException when accessing the map entries, making it a critical bug that needs fixing.

High
Unsafe cast to nested typed map

The cast to nested MutableMap<String, MutableMap<String, ActionRunResult>> is
unsafe. The readMapAsMutableMap() returns MutableMap<String, Any>, and casting to a
nested typed structure will fail at runtime when accessing nested values.

src/main/kotlin/org/opensearch/commons/alerting/model/BucketLevelTriggerRunResult.kt [28]

-sin.readMapAsMutableMap() as MutableMap<String, MutableMap<String, ActionRunResult>>
+sin.readMap(StreamInput::readString) { s -> 
+    s.readMap(StreamInput::readString, ActionRunResult::readFrom) 
+}?.toMutableMap() ?: mutableMapOf()
Suggestion importance[1-10]: 9

__

Why: The cast to nested MutableMap<String, MutableMap<String, ActionRunResult>> is unsafe. The readMapAsMutableMap() returns MutableMap<String, Any>, and this cast will fail when accessing nested values, causing runtime errors.

High
Unsafe cast to typed trigger result

The cast to Map<String, TriggerResult> is unsafe since readMapAsMutableMap() returns
MutableMap<String, Any>. The values are not typed as TriggerResult instances and
will cause ClassCastException when accessed. Use a typed read method instead.

src/main/kotlin/org/opensearch/commons/alerting/model/MonitorRunResult.kt [40]

-sin.readMapAsMutableMap() as Map<String, TriggerResult>
+sin.readMap(StreamInput::readString) { s -> 
+    TriggerRunResult.readFrom(s) as TriggerResult 
+} ?: mutableMapOf()
Suggestion importance[1-10]: 9

__

Why: The cast to Map<String, TriggerResult> is unsafe since readMapAsMutableMap() returns MutableMap<String, Any>. This will cause ClassCastException when accessing map values, representing a critical type safety issue.

High
Unsafe cast to MutableMap

The cast to MutableMap<String, String> is unsafe and can fail at runtime if the
deserialized map contains non-String values. Consider validating the map entries
before casting or using a safer conversion method that filters entries by type.

src/main/kotlin/org/opensearch/commons/alerting/model/MonitorMetadata.kt [40]

-sourceToQueryIndexMapping = (sin.readMap()?.toMutableMap() ?: mutableMapOf()) as MutableMap<String, String>
+sourceToQueryIndexMapping = sin.readMap()?.mapNotNull { (k, v) -> 
+    if (k is String && v is String) k to v else null 
+}?.toMap()?.toMutableMap() ?: mutableMapOf()
Suggestion importance[1-10]: 8

__

Why: The unsafe cast to MutableMap<String, String> can fail at runtime if the deserialized map contains non-String values. This is a critical type safety issue that could cause ClassCastException in production.

Medium

Previous suggestions

Suggestions up to commit 504f10c
CategorySuggestion                                                                                                                                    Impact
Possible issue
Unsafe cast to complex type

The cast to Map<String, ChainedAlertTriggerRunResult> is unsafe and will fail at
runtime since readMap() returns a map of basic types, not complex
ChainedAlertTriggerRunResult objects. Use readMap(StreamInput::readString,
ChainedAlertTriggerRunResult::readFrom) instead.

src/main/kotlin/org/opensearch/commons/alerting/model/WorkflowRunResult.kt [38]

-triggerResults = (sin.readMap()?.toMutableMap() ?: mutableMapOf()) as Map<String, ChainedAlertTriggerRunResult>
+triggerResults = sin.readMap(StreamInput::readString, ChainedAlertTriggerRunResult::readFrom) ?: mapOf()
Suggestion importance[1-10]: 9

__

Why: The cast to Map<String, ChainedAlertTriggerRunResult> is fundamentally incorrect because readMap() without type parameters returns a map of basic types, not complex Writeable objects. This will fail at runtime. Using readMap(StreamInput::readString, ChainedAlertTriggerRunResult::readFrom) is the correct approach for deserializing complex types.

High
Unsafe cast to MutableMap

The cast to MutableMap<String, String> is unsafe and may fail at runtime if the map
contains non-String values. Consider using mapValues or explicit type checking to
ensure type safety before casting.

src/main/kotlin/org/opensearch/commons/alerting/model/MonitorMetadata.kt [40]

-sourceToQueryIndexMapping = (sin.readMap()?.toMutableMap() ?: mutableMapOf()) as MutableMap<String, String>
+sourceToQueryIndexMapping = sin.readMap()?.mapValues { it.value as String }?.toMutableMap() ?: mutableMapOf()
Suggestion importance[1-10]: 8

__

Why: The unsafe cast to MutableMap<String, String> can fail at runtime if the map contains non-String values. The suggested approach using mapValues with explicit type checking provides better type safety and prevents potential ClassCastException.

Medium
General
Remove redundant type cast

The explicit cast is redundant since readMap(StreamInput::readString,
DocumentLevelTriggerRunResult::readFrom) already returns the correct type. Remove
the cast to simplify the code.

src/main/kotlin/org/opensearch/commons/alerting/action/DocLevelMonitorFanOutResponse.kt [36-37]

-triggerResults = (sin.readMap(StreamInput::readString, DocumentLevelTriggerRunResult::readFrom) ?: mapOf())
-    as Map<String, DocumentLevelTriggerRunResult>
+triggerResults = sin.readMap(StreamInput::readString, DocumentLevelTriggerRunResult::readFrom) ?: mapOf()
Suggestion importance[1-10]: 5

__

Why: The explicit cast is redundant since readMap(StreamInput::readString, DocumentLevelTriggerRunResult::readFrom) already returns Map<String, DocumentLevelTriggerRunResult>?. Removing it improves code clarity and reduces unnecessary complexity.

Low
Suggestions up to commit 9e0e029
CategorySuggestion                                                                                                                                    Impact
General
Remove redundant type cast

The cast to Map<String, DocumentLevelTriggerRunResult> is redundant since readMap
with typed readers already returns the correct type. Remove the unnecessary cast to
improve code clarity.

src/main/kotlin/org/opensearch/commons/alerting/action/DocLevelMonitorFanOutResponse.kt [36-37]

-triggerResults = (sin.readMap(StreamInput::readString, DocumentLevelTriggerRunResult::readFrom) ?: mapOf())
-    as Map<String, DocumentLevelTriggerRunResult>
+triggerResults = sin.readMap(StreamInput::readString, DocumentLevelTriggerRunResult::readFrom) ?: mapOf()
Suggestion importance[1-10]: 8

__

Why: The cast is indeed redundant since readMap with typed readers (StreamInput::readString, DocumentLevelTriggerRunResult::readFrom) already returns Map<String, DocumentLevelTriggerRunResult>?. Removing it improves code clarity and eliminates the need for @Suppress("UNCHECKED_CAST").

Medium
Possible issue
Unsafe cast to MutableMap

The unsafe cast to MutableMap<String, String> can fail at runtime if the map
contains non-String values. Consider using filterIsInstance or explicit type
checking to ensure type safety before casting.

src/main/kotlin/org/opensearch/commons/alerting/model/MonitorMetadata.kt [40]

-sourceToQueryIndexMapping = (sin.readMap()?.toMutableMap() ?: mutableMapOf()) as MutableMap<String, String>
+sourceToQueryIndexMapping = sin.readMap()?.let { map ->
+    map.entries.associate { (k, v) -> k.toString() to v.toString() }.toMutableMap()
+} ?: mutableMapOf()
Suggestion importance[1-10]: 7

__

Why: The unsafe cast can cause ClassCastException at runtime if the map contains non-String values. The suggestion provides a safer approach using explicit type conversion, though it assumes toString() is appropriate for all values.

Medium
Unsafe cast to ChainedAlertTriggerRunResult map

The unsafe cast to Map<String, ChainedAlertTriggerRunResult> can cause
ClassCastException at runtime if the map values are not of the expected type. Use a
safe casting approach with type validation.

src/main/kotlin/org/opensearch/commons/alerting/model/WorkflowRunResult.kt [38]

-triggerResults = (sin.readMap()?.toMutableMap() ?: mutableMapOf()) as Map<String, ChainedAlertTriggerRunResult>
+triggerResults = sin.readMap()?.mapNotNull { (k, v) ->
+    (k as? String)?.let { key -> (v as? ChainedAlertTriggerRunResult)?.let { value -> key to value } }
+}?.toMap() ?: mapOf()
Suggestion importance[1-10]: 7

__

Why: The unsafe cast to Map<String, ChainedAlertTriggerRunResult> can cause ClassCastException at runtime. The suggested approach with mapNotNull and safe casting provides better type safety, though it silently drops invalid entries.

Medium
Suggestions up to commit 9511928
CategorySuggestion                                                                                                                                    Impact
Possible issue
Use typed readMap with deserializers

The unsafe cast to Map<String, ChainedAlertTriggerRunResult> can fail if the
deserialized map contains incompatible types. Consider using a typed readMap
overload with custom deserializers for both key and value, similar to the approach
in DocLevelMonitorFanOutResponse.

src/main/kotlin/org/opensearch/commons/alerting/model/WorkflowRunResult.kt [37]

-triggerResults = (sin.readMap()?.toMutableMap() ?: mutableMapOf()) as Map<String, ChainedAlertTriggerRunResult>
+triggerResults = sin.readMap(StreamInput::readString, ChainedAlertTriggerRunResult::readFrom) ?: mapOf()
Suggestion importance[1-10]: 7

__

Why: This is a valid improvement that suggests using typed readMap with custom deserializers instead of an unsafe cast, making it consistent with the approach in DocLevelMonitorFanOutResponse. This would eliminate the need for the unsafe cast and improve type safety.

Medium
Validate map entry types safely

The unsafe cast to MutableMap<String, String> can cause ClassCastException at
runtime if the map contains non-String keys or values. Add explicit type validation
or use filterIsInstance to ensure type safety before casting.

src/main/kotlin/org/opensearch/commons/alerting/model/MonitorMetadata.kt [40]

-sourceToQueryIndexMapping = (sin.readMap()?.toMutableMap() ?: mutableMapOf()) as MutableMap<String, String>
+sourceToQueryIndexMapping = sin.readMap()?.entries
+    ?.associate { (k, v) -> (k as? String ?: throw IllegalStateException("Invalid key type")) to (v as? String ?: throw IllegalStateException("Invalid value type")) }
+    ?.toMutableMap() ?: mutableMapOf()
Suggestion importance[1-10]: 4

__

Why: While the unsafe cast to MutableMap<String, String> could theoretically cause a ClassCastException, the suggestion adds significant complexity. The proposed validation is overly verbose for what should be a straightforward deserialization, though it does address a legitimate type safety concern.

Low
Validate types before unsafe cast

The unsafe cast to Map<String, DocumentLevelTriggerRunResult> can fail at runtime if
the map contains unexpected types. Since readMap with custom deserializers should
return the correct type, verify the deserializer functions return the expected types
or add runtime type validation before casting.

src/main/kotlin/org/opensearch/commons/alerting/action/DocLevelMonitorFanOutResponse.kt [36-37]

-triggerResults = (sin.readMap(StreamInput::readString, DocumentLevelTriggerRunResult::readFrom) ?: mapOf())
-    as Map<String, DocumentLevelTriggerRunResult>,
+triggerResults = sin.readMap(StreamInput::readString, DocumentLevelTriggerRunResult::readFrom)
+    ?.mapValues { it.value as? DocumentLevelTriggerRunResult ?: throw IllegalStateException("Invalid trigger result type") }
+    ?: mapOf(),
Suggestion importance[1-10]: 3

__

Why: The suggestion addresses a potential runtime issue with unsafe casting, but the readMap with custom deserializers (StreamInput::readString, DocumentLevelTriggerRunResult::readFrom) should already ensure type safety. The proposed validation adds complexity without clear evidence of a real problem in the PR context.

Low

The constructor-level @Suppress("UNCHECKED_CAST") was missing even though
the deserialization expression contains an explicit unchecked cast of a
Map<String, TriggerRunResult> to Map<String, ChainedAlertTriggerRunResult>.

Kotlin requires the annotation to acknowledge the cast is intentionally
unsafe; omitting it produces a compiler warning. The cast is safe in
practice because all values were written as ChainedAlertTriggerRunResult
instances by writeTo().

Signed-off-by: thecodingshrimp <leonard.stutzer@sap.com>
@thecodingshrimp

thecodingshrimp commented Jun 9, 2026

Copy link
Copy Markdown
Author

Thanks for the automated review. Addressing each flagged item (via claude):

Unsafe cast: `WorkflowRunResult.kt` → `Map<String, ChainedAlertTriggerRunResult>`

The bot suggested switching to the typed `readMap(StreamInput::readString, ChainedAlertTriggerRunResult::readFrom)` overload to avoid the unchecked cast.

This is not viable without also changing the write side — the existing `out.writeMap(triggerResults)` uses the generic (type-tagged) wire format, while the typed `readMap` overload expects the type-free format written by `out.writeMap(map, keyWriter, valueWriter)`. Changing both sides in the same PR would introduce a wire-format break for rolling upgrades. The wire format for this map was established in its original introduction; preserving it is necessary.

What I've done instead (commit 9e0e029): added the missing `@Suppress("UNCHECKED_CAST")` on the constructor, which was inadvertently omitted. The cast is safe because the values were serialized as `ChainedAlertTriggerRunResult` instances by `writeTo()`.

Unsafe cast: `MonitorMetadata.kt` → `MutableMap<String, String>`

The suggestion to add per-entry type validation via `as? String ?: throw IllegalStateException` is over-defensive. The data at this position was always written by `MonitorMetadata.writeTo()` which casts `sourceToQueryIndexMapping: MutableMap<String, String>` down to `MutableMap<String, Any>` for the generic writer. The types are guaranteed by the write side. Adding entry-level type assertions would obscure a straightforward deserialization with complexity that adds no real value.

Unsafe cast: `DocLevelMonitorFanOutResponse.kt` → `Map<String, DocumentLevelTriggerRunResult>`

The suggestion to replace the cast with `.mapValues { it.value as? DocumentLevelTriggerRunResult ?: throw ... }` is redundant: `readMap(StreamInput::readString, DocumentLevelTriggerRunResult::readFrom)` already guarantees each value was deserialized via `DocumentLevelTriggerRunResult::readFrom`. A subsequent runtime check on what the typed deserializer just produced adds only overhead.

In summary: the flagged casts are all inherently unchecked due to JVM type erasure, but each is provably safe given the matching write side. The only actionable item was the missing `@Suppress` annotation in `WorkflowRunResult`.

@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 9e0e029

…tion

StreamInput.readMap() may return Collections.emptyMap() (immutable) for
zero-size maps. Before this fix, several deserialization constructors
assumed the result was always mutable, causing ClassCastException or
UnsupportedOperationException at runtime.

Tests cover the empty-map (regression) path for all six fixed call sites:
- Monitor.uiMetadata
- MonitorMetadata.lastRunContext + sourceToQueryIndexMapping
- IndexExecutionContext.lastRunContext + updatedLastRunContext
- DocLevelMonitorFanOutResponse.lastRunContexts
- WorkflowRunResult.triggerResults
- Alert.queryResults (each element map)

Each test serializes an object with an empty map via writeTo(), deserializes
via the StreamInput constructor, and asserts the result is correct and the
map is mutable.

Signed-off-by: thecodingshrimp <leonard.stutzer@sap.com>
@thecodingshrimp

Copy link
Copy Markdown
Author

Added regression tests in (commit 504f10c) covering all six fixed call sites. Each test exercises the empty-map path: serializes via writeTo(), deserializes via the StreamInput constructor, and asserts the round-trip succeeds and the result map is mutable.

@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 504f10c

executionId = sin.readString(),
monitorId = sin.readString(),
lastRunContexts = sin.readMap()!! as MutableMap<String, Any>,
lastRunContexts = sin.readMap()?.toMutableMap() ?: mutableMapOf(),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thecodingshrimp What do you think about using kotlin extensions here? Something like:

package org.opensearch.commons.alerting.util

import org.opensearch.core.common.io.stream.StreamInput
import org.opensearch.core.common.io.stream.Writeable

/**
 * Reads a map written by `StreamOutput.writeMap` and always returns a mutable map.
 *
 * Why this exists:
 * `StreamInput.readMap()` only guarantees the result implements `java.util.Map`. Its
 * Javadoc states that a non-empty result will be mutable, but an empty result *might*
 * be immutable (`Collections.emptyMap()`). This extension guarantees the returned
 * map is mutable.
 */
@Suppress("UNCHECKED_CAST")
fun StreamInput.readMapAsMutableMap(): MutableMap<String, Any> {
    val map = this.readMap() ?: return mutableMapOf()
    return if (map is MutableMap<*, *>) {
        map as MutableMap<String, Any>
    } else {
        // Immutable (e.g. Collections.emptyMap()) or unknown type: copy every entry
        // into a fresh mutable map.
        LinkedHashMap(map) as MutableMap<String, Any>
    }
}

/**
 * Typed variant for maps written with explicit key/value readers
 * (`StreamOutput.writeMap(map, keyWriter, valueWriter)`).
 */
fun <K, V> StreamInput.readMapAsMutableMap(
    keyReader: Writeable.Reader<K>,
    valueReader: Writeable.Reader<V>
): MutableMap<K, V> {
    val map = this.readMap(keyReader, valueReader) ?: return mutableMapOf()
    return if (map is MutableMap<K, V>) {
        map
    } else {
        LinkedHashMap(map)
    }
}

Then all the call sites that need a mutable map from StreamInput can use one of these methods.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion — agreed that centralizing this in an extension is the right call. I went with a cast-free version to avoid introducing a new @Suppress:

fun StreamInput.readMapAsMutableMap(): MutableMap<String, Any> =
    readMap()?.toMutableMap() ?: mutableMapOf()

This is placed in util/StreamInputExtensions.kt and used across all the fixed call sites. The is MutableMap<*, *> branch check in your proposal avoids an unnecessary copy for non-empty maps, but toMutableMap() is safe and keeps the helper annotation-free. I also extended the fix to cover the remaining suppressWarning(sin.readMap()) calls across the TriggerRunResult family and deleted the now-dead suppressWarning helpers.

- Add StreamInput.readMapAsMutableMap() extension (cast-free, no @Suppress)
- Replace suppressWarning(sin.readMap()) in MonitorRunResult.kt with extension
- Narrow @Suppress("UNCHECKED_CAST") to specific cast line in DocLevelMonitorFanOutResponse.kt
- Add regression tests for MonitorRunResult/InputRunResults empty-map paths
StreamInput.readMap(Reader, Reader) returns Collections.emptyMap() for
zero-size maps, same as the untyped overload. Replace with an explicit
empty-check that returns mutableMapOf(), and wrap non-empty results in
HashMap to guarantee mutability. The @Suppress("UNCHECKED_CAST") is
retained narrowly on the cast from TriggerRunResult to
DocumentLevelTriggerRunResult, which is unavoidable because readFrom
declares TriggerRunResult as its return type. Add regression test for
the empty triggerResults path.
- Fix QueryLevelTriggerRunResult, ClusterMetricsTriggerRunResult,
  ChainedAlertTriggerRunResult, BucketLevelTriggerRunResult: replace
  sin.readMap() as MutableMap cast with sin.readMapAsMutableMap()
- Fix ActionRunResult.output: replace suppressWarning(sin.readMap())
  with sin.readMapAsMutableMap(); delete now-unused suppressWarning helper
- Delete dead suppressWarning helpers in TriggerRunResult and Workflow
- Add regression tests for all fixed deserialization paths
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix ClassCastException from immutable map returned by StreamInput.readMap()

2 participants