Spring MVC (RESTful API): Validating payload dependent on a path variable
Asked Answered
W

2

6

Use Case:

  • let's design a RESTful create operation using POST HTTP verb - creating tickets where creator (assigner) specifies a ticket assignee
  • we're creating a new "ticket" on following location: /companyId/userId/ticket
  • we're providing ticket body containing assigneeId:

    { "assigneeId": 10 }

  • we need to validate that assigneeId belongs to company in URL - companyId path variable

So far:

@RequestMapping(value="/{companyId}/{userId}/ticket", method=POST)
public void createTicket(@Valid @RequestBody Ticket newTicket, @PathVariable Long companyId, @PathVariable Long userId) {
  ...
}
  • we can easily specify a custom Validator (TicketValidator) (even with dependencies) and validate Ticket instance
  • we can't easily pass companyId to this validator though! We need to verify that ticket.assigneeId belongs to company with companyId.

Desired output:

  • ability to access path variables in custom Validators

Any ideas how do I achieve the desired output here?

Weldonwelfare answered 28/10, 2015 at 21:33 Comment(3)
"we need to validate that assigneeId belongs to company in URL - companyId path variable" => so Ticket has assigneeId property and we need to ask database whether assigneeId belongs to a company with companyId. So we need both Ticket and companyId. Makes sense?Weldonwelfare
Sure. I missed that.Aynat
No worries, I updated the original Q (so hopefully it's clearer now).Weldonwelfare
R
2

If we assume that our custom validator knows desired property name, then we can do something like this:

Approach one:

1) We can move this getting path variables logic to some kind of a base validator:

public abstract class BaseValidator implements Validator {

    @Override
    public boolean supports(Class<?> clazz)
    {
        // supports logic
    }

    @Override
    public void validate(Object target, Errors errors)
    {
        // some base validation logic or empty if there isn't any
    }

    protected String getPathVariable(String name) {
        // Getting current request (Can be autowired - depends on your implementation)
        HttpServletRequest req = HttpServletRequest((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        if (req != null) {
            // getting variables map from current request
            Map<String, String> variables = req.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
            return variables.get(name);
        }
        return null;
    }
}

2) Extend it with your TicketValidator implementation:

public class TicketValidator extends BaseValidator {

    @Override
    public void validate(Object target, Errors errors)
    {
        // Getting our companyId var
        String companyId = getPathVariable("companyId");
        ...
        // proceed with your validation logic. Note, that all path variables
        // is `String`, so you're going to have to cast them (you can do 
        // this in `BaseValidator` though, by passing `Class` to which you 
        // want to cast it as a method param). You can also get `null` from 
        // `getPathVariable` method - you might want to handle it too somehow
    }
}

Approach two:

I think it worth to mention that you can use @PreAuthorize annotation with SpEL to do this kind of validation (You can pass path variables and request body to it). You'll be getting HTTP 403 code though if validation woudnt pass, so I guess it's not exaclty what you want.

Refurbish answered 8/8, 2017 at 12:58 Comment(0)
G
1

You could always do this:

@Controller
public class MyController {

    @Autowired
    private TicketValidator ticketValidator;

    @RequestMapping(value="/{companyId}/{userId}/ticket", method=POST)
    public void createTicket(@RequestBody Ticket newTicket,
            @PathVariable Long companyId, @PathVariable Long userId) {

        ticketValidator.validate(newTicket, companyId, userId);
        // do whatever

    }

}

Edit in response to the comment:

It doesn't make sense to validate Ticket independently of companyId when the validity of Ticket depends on companyId.

If you cannot use the solution above, consider grouping Ticket with companyId in a DTO, and changing the mapping like this:

@Controller
public class MyController {

    @RequestMapping(value="/{userId}/ticket", method=POST)
    public void createTicket(@Valid @RequestBody TicketDTO ticketDto,
            @PathVariable Long userId) {

        // do whatever
    }

}

public class TicketDTO {

    private Ticket ticket;

    private Long companyId;

    // setters & getters

}
Gyroplane answered 28/10, 2015 at 21:54 Comment(3)
Absolutely, but I was trying to reduce the amount of controller's responsibilities and have these standalone validators invoked automatically by the framework.Weldonwelfare
@Weldonwelfare I have edited the answer with another option which reduces the controller's responsibilities.Gyroplane
thanks, I already though of that - but this would break my API clients which is off the table for now :/ (contract relies on using Ticket, not on TicketDTO)Weldonwelfare

© 2022 - 2024 — McMap. All rights reserved.