micronaut-core icon indicating copy to clipboard operation
micronaut-core copied to clipboard

Form data parameter names are being transformed when the form is bound to a POJO

Open bivapa opened this issue 1 year ago • 6 comments

Expected Behavior

When I bind form data to a POJO, I expect the original parameter names to be preserved for deserialization.

Actual Behaviour

When I bind form data to a POJO, the original parameter names are decapitalized and dehyphenated before deserialization occurs.

Steps To Reproduce

Controller:

package com.example;

import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Post;

@Controller("/user")
public class UserController {

    @Post(consumes = MediaType.APPLICATION_FORM_URLENCODED)
    public String user(@Body User user) {
        return user.toString();
    }
}

POJO:

package com.example;

import com.fasterxml.jackson.annotation.JsonProperty;

public class User {
    @JsonProperty("UserId")
    private String uppercase;

    @JsonProperty("userId")
    private String lowercase;

    @Override
    public String toString() {
        return "User [uppercase=" + uppercase + ", lowercase=" + lowercase + "]";
    }
}
  1. Send the form data 'UserId=Bob' to the endpoint curl --location --request POST 'localhost:8080/user' --header 'Content-Type: application/x-www-form-urlencoded' --data-urlencode 'UserId=Bob'
  2. Result is 'User [uppercase=null, lowercase=Bob]' although according to the bindings the uppercase field should be filled with the value

Full example application: https://github.com/bivapa/micronaut-form-data-binding-bug/blob/main/src/test/java/com/example/UserControllerTest.java

After debugging I found that the form data is passing through the TypeConverter<Map, Object> instance registered by JsonConverterRegistrar (micronaut-json-core) which basically performs decapitalization and dehyphenation on each key thus producing 'userId' key out of the original 'UserId' parameter.

Environment Information

No response

Example Application

https://github.com/bivapa/micronaut-form-data-binding-bug

Version

3.5.4

bivapa avatar Aug 03 '22 11:08 bivapa

Please note that @JsonPropery is only used for JSON payloads. You are sending a form url encoded request

sdelamo avatar Aug 05 '22 07:08 sdelamo

According to the documentation, jackson bindings for form data should work the same as for JSON payloads. https://docs.micronaut.io/latest/guide/#formData

In practice this means that to bind regular form data, the only change required to the previous JSON binding code is updating the MediaType consumed

But in general, this is not about the @JsonProperty annotation specifically. The structure fed to deserialization always contains already transformed keys. For example, for the following custom jackson deserializer the result is the same.

public static class UserDeserializer extends StdDeserializer<User> { 

    public UserDeserializer() { 
        this(null); 
    } 

    public UserDeserializer(Class<?> vc) { 
        super(vc); 
    }

    @Override
    public User deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        JsonNode node = jp.getCodec().readTree(jp);
        User user = new User();
        if (node.get("userId") != null) {
            user.setLowercase(node.get("userId").asText());
        }
        if (node.get("UserId") != null) {
            user.setUppercase(node.get("UserId").asText());
        }
        return user;
    }
}

bivapa avatar Aug 05 '22 08:08 bivapa

Please, note that @JsonPropertyhas never worked for form data. https://github.com/micronaut-projects/micronaut-core/issues/1853

sdelamo avatar Aug 07 '22 07:08 sdelamo

I am not sure we should support JSON annotations for things that are not JSON payloads.

sdelamo avatar Aug 07 '22 07:08 sdelamo

it works for the server i think

yawkat avatar Aug 08 '22 07:08 yawkat

Interestingly, in the AWS api proxy lambda handler a form request is handled slightly differently (rather more like what I would expect here). For a lambda event, the form is passed directly to the bean binder as a map after it has been decoded from a string like "param=value" MicronautLambdaContainerHandler.java#L559, while for the Netty request case, it is passed to the binder via a Map/Object converter undergoing the transformation for its keys JsonConverterRegistrar.java#L173.

bivapa avatar Aug 18 '22 17:08 bivapa