1. Overview
In this tutorial, weβre going to take a look at defining custom media types and producing them by Spring REST controller.
A good use case for using custom media type is versioning an API.
2. API β Version 1
Letβs start with a simple example β an API exposing a single Resource by id.
Weβre going to start with a Version 1 of the Resource weβre exposing to the client. In order to do that, weβre going to use a custom HTTP header β βapplication/vnd.baeldung.api.v1+jsonβ.
The client will ask for this custom media type via the Accept header.
Hereβs our simple endpoint:
@RequestMapping(
method = RequestMethod.GET,
value = "/public/api/items/{id}",
produces = "application/vnd.baeldung.api.v1+json"
)
@ResponseBody
public BaeldungItem getItem( @PathVariable("id") String id ) {
return new BaeldungItem("itemId1");
}
Notice the produces parameter here β specifying the custom media type that this API is able to handle.
Now, the BaeldungItem Resource β which has a single field β itemId:
public class BaeldungItem {
private String itemId;
// standard getters and setters
}
Last but not least letβs write an integration test for endpoint:
@Test
public void givenServiceEndpoint_whenGetRequestFirstAPIVersion_then200() {
given()
.accept("application/vnd.baeldung.api.v1+json")
.when()
.get(URL_PREFIX + "/public/api/items/1")
.then()
.contentType(ContentType.JSON).and().statusCode(200);
}
3. API β Version 2
Now letβs assume that we need to change the details that weβre exposing to the client with our Resource.
We used to expose a raw id β letβs say that now we need to hide that and expose a name instead, to get a bit more flexibility.
Itβs important to understand that this change is not backwards compatible; basically β itβs a breaking change.
Hereβs our new Resource definition:
public class BaeldungItemV2 {
private String itemName;
// standard getters and setters
}
And so, what weβll need to do here is β migrate our API to a second version.
Weβre going to do that by creating the next version of our custom media type and defining a new endpoint:
@RequestMapping(
method = RequestMethod.GET,
value = "/public/api/items/{id}",
produces = "application/vnd.baeldung.api.v2+json"
)
@ResponseBody
public BaeldungItemV2 getItemSecondAPIVersion(@PathVariable("id") String id) {
return new BaeldungItemV2("itemName");
}
And so we now have the exact same endpoint, but capable of handling the new V2 operation.
When the client will ask for βapplication/vnd.baeldung.api.v1+jsonβ β Spring will delegate to the old operation and the client will receive a BaeldungItem with a itemId field (V1).
But when the client now sets the Accept header to βapplication/vnd.baeldung.api.v2+jsonβ β theyβll correctly hit the new operation and get back the Resource with the itemName field (V2):
@Test
public void givenServiceEndpoint_whenGetRequestSecondAPIVersion_then200() {
given()
.accept("application/vnd.baeldung.api.v2+json")
.when()
.get(URL_PREFIX + "/public/api/items/2")
.then()
.contentType(ContentType.JSON).and().statusCode(200);
}
Note how the test is similar but is using the different Accept header.
4. Custom Media Type on Class Level
Finally, letβs talk about a class-wide definition of the media type β thatβs possible as well:
@RestController
@RequestMapping(
value = "/",
produces = "application/vnd.baeldung.api.v1+json"
)
public class CustomMediaTypeController
As expected, the @RequestMapping annotation easily works on class level and allows us to specify the value, produces and consumes parameters.
5. Conclusion
This articles illustrated examples when defining Custom Media Types could be useful in versioning public API.
