java 98 lines · 4 tabs

Version a Spring Boot REST API via Content Negotiation and Custom Media Types

Shared by codesnips Aug 2026
4 tabs
package com.shop.api.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer
            .favorParameter(false)
            .ignoreAcceptHeader(false)
            .defaultContentType(MediaType.parseMediaType("application/vnd.shop.order.v1+json"));
    }
}
4 files · java Explain with highlit

API versioning by content negotiation keeps a single, stable URL (/api/orders/{id}) while letting clients opt into a specific representation through the Accept header. Instead of baking /v1/ or /v2/ into the path, each version is expressed as a vendor media type such as application/vnd.shop.order.v1+json. This snippet shows how Spring Boot's produces attribute on request mappings dispatches to different handler methods purely based on the requested media type, so old and new clients coexist on one endpoint.

In OrderMediaTypes, the vendor types are declared as constants and parsed into MediaType instances once, avoiding scattered string literals and giving a single source of truth. The V1_JSON and V2_JSON values follow the vnd.<vendor>.<resource>.<version>+json convention, which is the idiomatic way to encode a semantic version in a MIME type while still telling parsers the payload is JSON.

OrderController maps both versions to the same URL. The two getOrder methods differ only by their produces value, and Spring's ContentNegotiationManager routes the request to whichever method matches the client's Accept header. The default method (annotated with V1_JSON) is also marked so that a client sending a generic application/json or no preference falls back to v1, preserving backward compatibility. Each method delegates to OrderService for the domain object and then maps it to a version-specific DTO.

The DTOs in OrderResponses are where the contract actually diverges. OrderV1 exposes a flat customerName, while OrderV2 splits it into a structured customer object and adds a currency field — a breaking change that would otherwise force a new URL. Keeping these as separate immutable records means the shape of each version is explicit and cannot drift.

The main trade-off is discoverability: version-in-header is invisible in a browser address bar and harder to test with a naive curl, so tooling must set Accept deliberately. In return, URLs stay clean and resources keep one canonical identity. This pattern suits public APIs with long-lived clients where breaking changes are rare but must be introduced without stranding existing integrations. A pitfall to watch is forgetting the fallback mapping, which would return 406 Not Acceptable to legacy callers.


Related snips

Share this code

Here's the card — post it anywhere.

Version a Spring Boot REST API via Content Negotiation and Custom Media Types — share card
Link copied