micronaut-core
micronaut-core copied to clipboard
Form data parameter names are being transformed when the form is bound to a POJO
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 + "]";
}
}
- 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'
- 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
Please note that @JsonPropery is only used for JSON payloads. You are sending a form url encoded request
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;
}
}
Please, note that @JsonProperty
has never worked for form data. https://github.com/micronaut-projects/micronaut-core/issues/1853
I am not sure we should support JSON annotations for things that are not JSON payloads.
it works for the server i think
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.