RayS
RayS

Reputation: 41

Using content negotiation with Spring MVC for REST API versioning

I am trying to use content negotiation to implement API versioning since putting a version in the URI is not RESTful. It doesn't seem possible with Spring MVC since the consumes/produces attributes of the @RequestMapping are not taken into account when resolving controller methods, just the path. For the sake of discussion let's say I have this controller

@RestController
@RequestMapping(path = "/foo")
public class FooController {
    @RequestMapping(path = "{id}", method = RequestMethod.GET, produces = { MediaType.APPLICATION_JSON_VALUE })
    @ResponseBody
    public Foo getFoo(@PathVariable Long id) {
        return repository.findOne(id);
    }

    @RequestMapping(path = "{id}", method = RequestMethod.GET, produces = { "application/vnd.com.me.model.v1+json" })
    @ResponseBody
    public FooV1 getFooV1(@PathVariable Long id) {
        return repositoryV1.findOne(id);
    }
    ...
}

The REST API is "GET /foo/1" or "GET /foo/2" etc but I want to be able to send either

Accept: application/json

or

Accept: application/vnd.com.me.model.v1+json

and have it go to the correct controller method. Ignore the issue of how best to represent the model. This is just about getting Spring MVC to let me have a single REST URI handle different content types. This obviously applies to the other HTTP methods, PUT, POST, etc. as well. Given the above controller definition you get an error as Spring tries to resolve the mappings, some variation of "sorry there's already a mapping for '/foo/{id}'."

Have I missed something that will allow me to achieve this?

Upvotes: 3

Views: 1001

Answers (2)

RayS
RayS

Reputation: 41

OK, I found what was going wrong. My bad. The issue was that I had a DELETE method, which only takes an id of type long, so I did not have a produces/consumes declaration which therefore did result in an "ambiguous" error due to 2 methods with the same request mapping.

glad to know Spring is working as I hoped/expected. :)

Upvotes: 1

Mathias Dpunkt
Mathias Dpunkt

Reputation: 12184

This should absolutely work as you described. I have a working example here:

@RestController
@RequestMapping(path = "/persons")
public class PersonController {

    private final PersonV1Repository personV1Repository;
    private final PersonV2Repository personV2Repository;

    @Autowired
    public PersonController(PersonV1Repository personV1Repository, PersonV2Repository personV2Repository) {
        this.personV1Repository = personV1Repository;
        this.personV2Repository = personV2Repository;
    }

    @RequestMapping(produces = "application/vnd.com.me.model.v1+json")
    public ResponseEntity<List<PersonV1>> getPersonsV1() {
        return ResponseEntity.ok(personV1Repository.findAll());
    }

    @RequestMapping(path = "/{id}", produces = "application/vnd.com.me.model.v1+json")
    public ResponseEntity<PersonV1> getPersonV1(@PathVariable Long id) {
        return ResponseEntity.ok(personV1Repository.findOne(id));
    }

    @RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<List<PersonV2>> getPersonsV2() {
        return ResponseEntity.ok(personV2Repository.findAll());
    }

    @RequestMapping(path = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<PersonV2> getPersonV2(@PathVariable Long id) {
        return ResponseEntity.ok(personV2Repository.findOne(id));
    }
}

I get the version I selected using the Accept header - here are example requests and responses:

http :8080/persons/2 Accept:application/vnd.com.me.model.v1+json -v
GET /persons/2 HTTP/1.1
Accept: application/vnd.com.me.model.v1+json
Accept-Encoding: gzip, deflate
Connection: keep-alive
Host: localhost:8080
User-Agent: HTTPie/0.9.2



HTTP/1.1 200 OK
Content-Type: application/vnd.com.me.model.v1+json;charset=UTF-8
Date: Mon, 07 Dec 2015 20:14:26 GMT
Server: Apache-Coyote/1.1
Transfer-Encoding: chunked
X-Application-Context: application

{
    "firstname": "some",
    "id": 2,
    "lastname": "other"
}

And getting the default version:

http :8080/persons/2 Accept:application/json -v
GET /persons/2 HTTP/1.1
Accept: application/json
Accept-Encoding: gzip, deflate
Connection: keep-alive
Host: localhost:8080
User-Agent: HTTPie/0.9.2



HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Date: Mon, 07 Dec 2015 20:16:20 GMT
Server: Apache-Coyote/1.1
Transfer-Encoding: chunked
X-Application-Context: application

{
    "id": 2,
    "name": "some"
}

There must be some other duplicate mapping in your controller - you can add the full controller code for further investigation.

Upvotes: 3

Related Questions