2

I'm trying to get the result of a JavaScript call to resemble a JSON structure of Map<String,Object> where the values may be number, string, boolean, objects (Maps) or arrays (Lists), similar to what Jackson does if you convert a value to a map.

When using the Value.as(Map.class) call I would expect the values in the Map to follow these rules

If the raw Map.class or an Object component type is used, then the return types of the the list are subject to Object target type mapping rules recursively.

Further down the documentation (rule 8 of Object mapping)

If the value has array elements and it has an array size that is smaller or equal than Integer.MAX_VALUE then the result value will implement List.

However this test fails

public class TestGraalMap {
    static String JS_CODE = "(function myFun(){ return { listProperty: ['listValue']};})";

    @Test
    public void testList() {
        try (Context context = Context.create()) {
            Value value = context.eval("js", JS_CODE);
            Value result = value.execute();
            Map<String,Object> resultMap = result.as(Map.class);
            assertThat(resultMap).hasEntrySatisfying("listProperty", testArray -> {
                assertThat(testArray).asList().containsExactly("listValue");
            });
        }
    }
}

with the following error.

 Expecting:
  <{}>
to be an instance of:
  <java.util.List>
but was instance of:
  <com.oracle.truffle.polyglot.PolyglotMap>

What am I missing?

Brecht Yperman
  • 1,209
  • 13
  • 27

1 Answers1

2

Yes there is the rule 8 about default object target coercions [1]. However these rules are executed in order. So just prior to that there is rule 7 which states if a Value has members then it will be converted to Map instead. And this rule applies to JavaScript objects as they have members and array elements at the same time. Arguably this behaviour is counter intuitive, but GraalVM inherited this from other engines like Nashorn. The GraalVM team is still discussing whether this behavior can change by default, but that is tbd.

In the meantime the polyglot API has a neat feature called target type mappings that you can use to customize these mappings the way you need them. Here is what you need in your case:

static String JS_CODE = "(function myFun(){ return { listProperty: ['listValue']};})";

@Test
public static void test {
    HostAccess access = HostAccess.newBuilder(HostAccess.EXPLICIT)
            .targetTypeMapping(
                    // for any conversion Any Value -> Object
                    Value.class, Object.class, 
                    // if the value has array elements and members
                    (v) -> v.hasArrayElements() && v.hasMembers(), 
                    // convert to List (instead of Map)
                    (v) -> v.as(List.class)).build();
    
    try (Context context = Context.newBuilder().allowHostAccess(access).build()) {
        Value value = context.eval("js", JS_CODE);
        Value result = value.execute();
        Map<String, Object> resultMap = result.as(Map.class);
        assertThat(resultMap).hasEntrySatisfying("listProperty", testArray -> {
            assertThat(testArray).asList().containsExactly("listValue");
        });
    }
}

[1] https://www.graalvm.org/sdk/javadoc/org/graalvm/polyglot/Value.html#as-java.lang.Class-

Christian Humer
  • 471
  • 3
  • 6