New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add @FormAttribute attributes to customize x-www-form-urlencoded [SPR-13433] #18012

Open
spring-issuemaster opened this Issue Sep 4, 2015 · 12 comments

Comments

Projects
None yet
1 participant
@spring-issuemaster
Copy link
Collaborator

spring-issuemaster commented Sep 4, 2015

Phil Webb opened SPR-13433 and commented

As requested on the Spring Boot issue tracker:
spring-projects/spring-boot#3890

When processing an HttpRequest with x-www-form-urlencoded content, we can use a controller with POJO matching the payload structure.

Example payload: value_first=value1&value_second=value2

@RequestMapping(method = RequestMethod.POST, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
 public String handlePost(@ModelAttribute Body body) {
 }

POJO:

public class Body {
 private String value_first;
 private String value_second;
}

The problem is that the variable names must match the field names in the HttpRequest payload in order to be processed by the controller. There is not way of naming them differently, for example, if I do not want to use underscores in Java variable and method names.

What I would appreciate would be using something like this:

@FormAttribute
private String value1;

Issue Links:

  • #13880 Provide a way to customize the names of JavaBean properties when they're read and written for data binding purposes
  • #12403 Provide support for configuring the bindable properties of a form-backing object using field-level annotations
  • #14816 Customizable parameter name when binding an object ("supersedes")

20 votes, 16 watchers

@spring-issuemaster

This comment has been minimized.

Copy link
Collaborator Author

spring-issuemaster commented Sep 8, 2015

Rossen Stoyanchev commented

Adding link to an (old) related ticket #13880.

Wouldn't that have to be @FormAttribute("value_first")? I'm wondering what's the idea. For such a cross-cutting requirement such as using underscores vs camelback notation, putting @FormAttribute on every field seems repetitive.

@spring-issuemaster

This comment has been minimized.

Copy link
Collaborator Author

spring-issuemaster commented Sep 10, 2015

Martin Myslík commented

I should have clarified this in my simplified example. It should , of course, be:

@FormAttribute("value_first")
private String value1;

As for the requirement, I am not saying that this is a core functionality of Spring that I am desperately missing but imagine this scenario:

You are processing a form-encoded payload from a third party service and you want to use POJO to map the values. You use camelCase in your whole project and now you are forced to use underscopes if the third party service does so (in the POJO) or write your own converter just for this purpose.

When I encountered this problem, I immediately looked for some Spring functionality to tackle this and was surprised that there is none. Feel free to close this issue if you feel that I am being unreasonable but I expected more people having this issue as well.

@spring-issuemaster

This comment has been minimized.

Copy link
Collaborator Author

spring-issuemaster commented Sep 11, 2015

Rossen Stoyanchev commented

I think the scenario is quite clear. There is nothing unreasonable about your request. I'm only wondering whether annotations would solve this effectively. An annotation here and there to customize a field name is okay but having to put one on every field to adapt to different naming strategy -- that feels more like something that should be more centralized, otherwise you'd have to have one on every field.

If the use case is purely input based, i.e. data binding from a request such as a REST service, then a Filter could wraps the request and overrides the getRequestParam() and related method to check for both "firstValue" and also "first_value".

@spring-issuemaster

This comment has been minimized.

Copy link
Collaborator Author

spring-issuemaster commented Sep 11, 2015

Martin Myslík commented

I had several use cases where I had to extract just several fields from a huge form-encoded request which would mean putting this annotation on a couple of properties but you are right that you still have to annotate every property of such POJO. Perhaps some higher level annotation for the whole classto automatically convert underscopes in the field names to camel case or a filter as you are suggesting would be more elegant solution.

@spring-issuemaster

This comment has been minimized.

Copy link
Collaborator Author

spring-issuemaster commented May 22, 2017

Micah Silverman commented

Bringing this up again. ;) There are still a number of important providers that insist on x-www-form-urlencoded when POSTing. Regarding the concern around lots of annotations on a POJO, @JsonProperty has worked very well for application/json type POSTs. It makes everything automatic, even if the result is every field having an annotation on it.

I recently encountered this directly in working with Slack's slash command features. https://api.slack.com/slash-commands. In short, you register an endpoint with Slack, and when you issue a "slash" command, slack POSTs to your endpoint with x-www-form-urlencoded Content-type.

So, one approach to handle that would look like this:

@RequestMapping(
  value = "/slack",
  method = RequestMethod.POST,
  consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE
)
public MyResponse onReceiveSlashCommand(
  @RequestParam("token") String token,
  @RequestParam("team_id") String teamId,
  @RequestParam("team_domain") String teamDomain,
  @RequestParam("channel_id") String channelId,
  @RequestParam("channel_name") String channelName,
  @RequestParam("user_id") String userId,
  @RequestParam("user_name") String userName,
  @RequestParam("command") String command,
  @RequestParam("text") String text,
  @RequestParam("response_url") String responseUrl
) {
    ...
}

This is pretty gnarly, especially in the age of Spring boot's built in Jackson mapper handling.

So, I set out to do this:

public class SlackSlashCommand {

    private String token;
    private String command;
    private String text;

    @JsonProperty("team_id")
    private String teamId;

    @JsonProperty("team_domain")
    private String teamDomain;

    @JsonProperty("channel_id")
    private String channelId;

    @JsonProperty("channel_name")
    private String channelName;

    @JsonProperty("user_id")
    private String userId;

    @JsonProperty("user_name")
    private String userName;

    @JsonProperty("response_url")
    private String responseUrl;

    ...
}

If the POST were sent as application/json, then the controller would look like this and we'd be done:

    @RequestMapping(value = "/slack", method = RequestMethod.POST)
    public @ResponseBody SlackSlashCommand slack(@RequestBody SlackSlashCommand slackSlashCommand) {
        log.info("slackSlashCommand: {}", slackSlashCommand);

        return slackSlashCommand;
    }

But, slack will only POST with x-www-form-urlencoded. So, I had to make the controller method like this:

    @RequestMapping(
        value = "/slack", method = RequestMethod.POST,
        consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, produces = MediaType.APPLICATION_JSON_VALUE
    )
    public @ResponseBody SlackSlashCommand slack(SlackSlashCommand slackSlashCommand) {
        log.info("slackSlashCommand: {}", slackSlashCommand);

        return slackSlashCommand;
    }

Only problem is that the underscore properties coming in from Slack get ignored when materializing the SlackSlashCommand. If there were an analog to @JsonProperty for form POSTs, then this would be handled automatically.

What I did for now to get around this, and so as to not pollute my SlackSlashCommand class, is a little ugly, but it works:

// workaround for customize x-www-form-urlencoded
public abstract class AbstractFormSlackSlashCommand {

    public void setTeam_id(String teamId) {
        setTeamId(teamId);
    }

    public void setTeam_domain(String teamDomain) {
        setTeamDomain(teamDomain);
    }

    public void setChannel_id(String channelId) {
        setChannelId(channelId);
    }

    public void setChannel_name(String channelName) {
        setChannelName(channelName);
    }

    public void setUser_id(String userId) {
        setUserId(userId);
    }

    public void setUser_name(String userName) {
        setUserName(userName);
    }

    public void setResponse_url(String responseUrl) {
        setResponseUrl(responseUrl);
    }

    abstract void setTeamId(String teamId);
    abstract void setTeamDomain(String teamDomain);
    abstract void setChannelId(String channelId);
    abstract void setChannelName(String channelName);
    abstract void setUserId(String userId);
    abstract void setUserName(String userName);
    abstract void setResponseUrl(String responseUrl);
}

public class SlackSlashCommand extends AbstractFormSlackSlashCommand {
...
}

That's a lot of boilerplate to accomplish what Jackson can do automatically with the @JsonProperty annotation! In fact, I left those annotations in so that I can receive the SlackSlashCommand object in the controller and return it as a @ResponseBody:

http -v -f POST localhost:8080/api/v1/slack2 token=token team_id=team_id team_domain=team_domain channel_id=channel_id channel_name=channel_name user_id=user_id user_name=user_name command=command text=text response_url=response_url
POST /api/v1/slack2 HTTP/1.1
Accept: */*
Content-Type: application/x-www-form-urlencoded; charset=utf-8
...

token=token&team_id=team_id&team_domain=team_domain&channel_id=channel_id&channel_name=channel_name&user_id=user_id&user_name=user_name&command=command&text=text&response_url=response_url

HTTP/1.1 200
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Type: application/json;charset=UTF-8
Date: Mon, 22 May 2017 05:28:27 GMT
...
{
    "channel_id": "channel_id",
    "channel_name": "channel_name",
    "command": "command",
    "response_url": "response_url",
    "team_domain": "team_domain",
    "team_id": "team_id",
    "text": "text",
    "token": "token",
    "user_id": "user_id",
    "user_name": "user_name"
}
@spring-issuemaster

This comment has been minimized.

Copy link
Collaborator Author

spring-issuemaster commented May 22, 2017

Micah Silverman commented

Two approaches to solve the problem. Both require more boilerplate than is necessary for accomplishing the same thing with JSON:

https://gist.github.com/dogeared/7e60a2caebd00c959f0d7e24ef79b54e
https://gist.github.com/dogeared/0db806ad56258711de0ffdfa5317ee42

This first approach uses an abstract super-class that breaks Java naming conventions. Pros: subclass is kept "clean". It's clear what's going on in the super class and why. No additional converters or configuration needed. Cons: Breaks Java naming conventions.

The second approach is the more "proper" approach. It uses an HttpMessageConverter. Pros: it's more idiomatic Spring and doesn't break any naming conventions. It enables using @RequestBody annotation, as a Spring developer would expect. Cons: more "manual labor" involved in building the POJO by hand.

@spring-issuemaster

This comment has been minimized.

Copy link
Collaborator Author

spring-issuemaster commented May 25, 2017

@spring-issuemaster

This comment has been minimized.

Copy link
Collaborator Author

spring-issuemaster commented May 25, 2017

@spring-issuemaster

This comment has been minimized.

Copy link
Collaborator Author

spring-issuemaster commented Dec 7, 2017

wu wen commented

+1

@spring-issuemaster

This comment has been minimized.

Copy link
Collaborator Author

spring-issuemaster commented Jan 6, 2018

haiepng liang commented

+1

@spring-issuemaster

This comment has been minimized.

Copy link
Collaborator Author

spring-issuemaster commented Feb 28, 2018

Iker Hernaez commented

+1

@spring-issuemaster

This comment has been minimized.

Copy link
Collaborator Author

spring-issuemaster commented May 29, 2018

Zhang Jie commented

Hi, all, I have found that, if we want to use custom @FormAttribute or something else to customize x-www-form-urlencoded, we can write a custom FormAttributeBeanInfoFactory which is similar to ExtendedBeanInfoFactory, and which can create FormAttributeBeanInfo similar to ExtendedBeanInfo and using @FormAttribute to get property name(by propertyNameFor()).
When we configure FormAttributeBeanInfoFactory into /META-INF/spring.factories with key org.springframework.beans.BeanInfoFactory, it will be used by WebDataBinder and BeanWrapperImpl, and will work as expected with @ModelAttribute.
I didn't test all test case, but i think it will be an alternative way to custom parameter name when binding an object.

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