spring-graphql icon indicating copy to clipboard operation
spring-graphql copied to clipboard

JSON scalar is not converted to Map

Open bduisenov opened this issue 3 years ago • 5 comments

I'm currently facing an issue when JSON scalar is not converted into a Map correctly.

To my understanding, the issue is with DataBinder that tries to convert the value. In case when the target type is a Map interface, it will fail on a line spring-graphql-1.0.1-sources.jar!/org/springframework/graphql/data/GraphQlArgumentBinder.java:248 with error saying that it cannot find constructor. And when switching to a concrete implementation like HashMap, an empty map will be a result of conversion because of BeanPropertyBindingResult.

I'm not sure whether this is an expected behaviour to be honest, but I assume that this is a bug as the conversion works in case when target type is List<Map<.., ..>>

bduisenov avatar Jul 21 '22 11:07 bduisenov

I have faced this same issue. I couldn't really find a solution other than implementing my own custom coercing approach (JsonNodeCoearcing) - instead of using Map I have used Jackson's JsonNode as target conversion type.

Check this question I posted on StackOverflow, I have also added the custom coercing solution details there: https://stackoverflow.com/questions/72915766/spring-graphql-how-to-map-json-extended-scalar-to-pojo-attribute

I'm not sure if there is an official approach to work around this and not requiring custom coercing impl.

djselzlein avatar Jul 27 '22 13:07 djselzlein

@bduisenov could you share a sample application showing the issue?

@djselzlein I think this might be a different problem. In your example, it looks like you're assuming that Jackson is used to deserialize the JSON input: it's not the case. GraphQL Java is itself deserializing the JSON input as Maps and applying scalars and coercion along the way. After that, Spring GraphQL will try to bind those values to POJOs. As a consequence, the @JsonDeserialize annotation on your POJO will never be used for deserialization. I think scalars and coercion are the expected way to go, although dealing with JsonNode objects as part of the input payload looks a bit strange to me.

bclozel avatar Jul 27 '22 14:07 bclozel

If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.

spring-projects-issues avatar Aug 03 '22 14:08 spring-projects-issues

Closing due to lack of requested feedback. If you would like us to look at this issue, please provide the requested information and we will re-open the issue.

spring-projects-issues avatar Aug 10 '22 14:08 spring-projects-issues

@bclozel here's a test and sample application showing (what I understand to be) the issue:

https://github.com/jjavery/json-scalar-bug/blob/main/src/test/java/com/example/jsonscalar/controller/ExampleControllerTests.java

In the schema, the CreateExampleInput input contains a JSON scalar named data. This should be converted to Map<String, Object> and assigned to the data property in a CreateExampleInput instance, but an empty HashMap<> is assigned instead.

The JSON scalar is from com.graphql-java:graphql-java-extended-scalars:18.1

jjavery avatar Aug 13 '22 21:08 jjavery

Thanks for the sample.

The underlying problem seems to be that our @Argument support doesn't support binding to Map. If the the Object has getters/setters for which we use a DataBinder, we use a regular nested path myMap.myKey instead of an indexed one myMap[myKey]. For an Object with constructor arg, we try to create and initialize as a regular Object, which doesn't work for a Map.

It would be relatively straight forward to address this for the constructor case because we know that the target type is Map so I'll schedule that for 1.0.3. It would be more difficult to address for the getter/setter case, because when we build the property paths for the data binding without awareness of the target field type. It's something that can be done but requires more work.

rstoyanchev avatar Oct 18 '22 11:10 rstoyanchev

For 1.0.x, this has been fixed for the constructor based initialization. That means, in the example from @jjavery, the CreateExampleInput type would change constructor injection:

@Data
@AllArgsConstructor
public class CreateExampleInput {
    private String name;
    private Map<String, Object> data;
}

For 1.1.x, we'll address this also for bean property initialization, in #516.

rstoyanchev avatar Oct 19 '22 10:10 rstoyanchev