Skip to content

[Bug]: ctx.map returns MapResult items as LinkedHashMap instead of the declared result type #870

Open

Description

@yaythomas

Expected Behavior

DurableContext.map(name, items, Class<O>, fn, config) returns a MapResult<O> whose items are instances of O. This holds on the first execution and on replay.

Actual Behavior

When O is a POJO or record, each item in the returned MapResult is a java.util.LinkedHashMap. Reading an item as O throws ClassCastException.

This happens on the first execution, not only on replay. On completion, MapOperation replaces its result with a serialize-then-deserialize copy, so the first execution returns the same value that replay returns.

Steps to Reproduce

public record Payload(String value) {}

@Test
void itemIsDeclaredType() {
    var runner = LocalDurableTestRunner.create(String.class, (input, context) -> {
        var result = context.map(
                "m",
                List.of("a", "b"),
                Payload.class,
                (item, index, ctx) -> new Payload(item),
                MapConfig.builder().build());
        Object first = result.getResult(0);
        return first.getClass().getName();
    });
    var result = runner.runUntilComplete("x");
    assertEquals(ExecutionStatus.SUCCEEDED, result.getStatus());
    assertEquals(Payload.class.getName(), result.getResult(String.class));
}

The test fails:

expected: <...$Payload> but was: <java.util.LinkedHashMap>

SDK Version

main at 3.0.1-SNAPSHOT. Also reported on 2.x. On 1.2.1 the first execution returns Payload, and replay of a completed map returns LinkedHashMap.

Java Version

17

Is this a regression?

Yes

Last Working Version

None fully. 1.2.1 is correct on the first execution and wrong on replay.

Additional Context

The cause is in MapOperation. The constructor passes new TypeToken<>() {} to its superclass as the result type (sdk/src/main/java/software/amazon/lambda/durable/operation/MapOperation.java, line 83).

  1. That anonymous class is created inside the generic class MapOperation<I, O>.
  2. So the captured type is MapResult<O>, with O still a type variable.
  3. So Jackson has no item type and deserializes each item as a LinkedHashMap.
  4. The constructor receives the caller's TypeToken<O> as itemResultType. It stores it but does not use it to build the result type.

The existing MapIntegrationTest cases use String and Integer items. Jackson deserializes those correctly without type information, so the tests do not catch the bug.

The JavaScript SDK also returns the serdes round-trip of the batch result on both paths. Its items are plain JSON objects, which already match the declared TypeScript type. So keeping the round-trip and fixing the item type keeps Java consistent with JavaScript.

Suggested fix: build a parameterized MapResult<O> type from itemResultType, for example by letting TypeToken be created from a java.lang.reflect.Type. Add integration tests with a POJO item type for the first execution and for replay.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingpkg:sdkModule: sdk

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions