or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

annotation-controllers.mdconfiguration.mdcontent-negotiation.mdexception-handling.mdfunctional-routing.mdindex.mdresource-handling.mdview-rendering.mdwebclient.mdwebsocket.md
tile.json

content-negotiation.mddocs/

Content Negotiation and API Versioning

Spring WebFlux provides flexible content negotiation to determine the best media type for responses based on client preferences. It also includes comprehensive API versioning support with multiple resolution strategies (header, query parameter, path, media type parameter) and deprecation handling.

Capabilities

Content Type Resolution

The RequestedContentTypeResolver interface determines the requested content types from a request.

@FunctionalInterface
public interface RequestedContentTypeResolver {
    // Resolve requested media types from the request
    List<MediaType> resolveMediaTypes(ServerWebExchange exchange);

    // Default content type when none specified
    default List<MediaType> getDefaultMediaTypes() {
        return Collections.emptyList();
    }
}

Content Type Resolver Builder

The RequestedContentTypeResolverBuilder builds a resolver with multiple resolution strategies.

public class RequestedContentTypeResolverBuilder {
    public RequestedContentTypeResolverBuilder() { ... }

    // Enable header-based resolution (Accept header)
    public RequestedContentTypeResolverBuilder headerResolver() { ... }

    // Enable parameter-based resolution with default parameter name "format"
    public RequestedContentTypeResolverBuilder parameterResolver() { ... }

    // Enable parameter-based resolution with custom parameter name
    public RequestedContentTypeResolverBuilder parameterResolver(String parameterName) { ... }

    // Add media type mapping for parameter values
    public RequestedContentTypeResolverBuilder mediaType(String key, MediaType mediaType) { ... }

    // Add multiple media type mappings
    public RequestedContentTypeResolverBuilder mediaTypes(Map<String, MediaType> mediaTypes) { ... }

    // Set fixed content type (ignores client preferences)
    public RequestedContentTypeResolverBuilder fixedResolver(MediaType... mediaTypes) { ... }

    // Set default content types
    public RequestedContentTypeResolverBuilder defaultContentTypes(MediaType... defaultMediaTypes) { ... }

    // Build the resolver
    public RequestedContentTypeResolver build() { ... }
}

Usage:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder;
import org.springframework.web.reactive.config.WebFluxConfigurer;

@Configuration
public class ContentNegotiationConfig implements WebFluxConfigurer {

    @Override
    public void configureContentTypeResolver(RequestedContentTypeResolverBuilder builder) {
        builder
            .headerResolver()
            .parameterResolver("format")
            .mediaType("json", MediaType.APPLICATION_JSON)
            .mediaType("xml", MediaType.APPLICATION_XML)
            .defaultContentTypes(MediaType.APPLICATION_JSON);
    }
}

// Example requests:
// GET /users - Uses Accept header
// GET /users?format=json - Returns JSON
// GET /users?format=xml - Returns XML

Header Content Type Resolver

The HeaderContentTypeResolver resolves content types from the Accept header.

public class HeaderContentTypeResolver implements RequestedContentTypeResolver {
    public HeaderContentTypeResolver() { ... }

    @Override
    public List<MediaType> resolveMediaTypes(ServerWebExchange exchange) { ... }
}

Usage:

import org.springframework.web.reactive.accept.HeaderContentTypeResolver;

RequestedContentTypeResolver resolver = new HeaderContentTypeResolver();

// Resolves from Accept header:
// Accept: application/json -> [application/json]
// Accept: text/html, application/xml -> [text/html, application/xml]
// Accept: */* -> []

Fixed Content Type Resolver

The FixedContentTypeResolver returns a fixed list of media types.

public class FixedContentTypeResolver implements RequestedContentTypeResolver {
    public FixedContentTypeResolver(List<MediaType> mediaTypes) { ... }

    @Override
    public List<MediaType> resolveMediaTypes(ServerWebExchange exchange) { ... }

    @Override
    public List<MediaType> getDefaultMediaTypes() { ... }
}

Usage:

import org.springframework.web.reactive.accept.FixedContentTypeResolver;
import org.springframework.http.MediaType;

// Always returns JSON regardless of client preferences
RequestedContentTypeResolver resolver = new FixedContentTypeResolver(
    List.of(MediaType.APPLICATION_JSON)
);

Parameter Content Type Resolver

The ParameterContentTypeResolver resolves content types from a query parameter.

public class ParameterContentTypeResolver implements RequestedContentTypeResolver {
    public static final String DEFAULT_PARAMETER_NAME = "format";

    public ParameterContentTypeResolver(Map<String, MediaType> mediaTypes) { ... }

    // Set parameter name
    public void setParameterName(String parameterName) { ... }

    // Get parameter name
    public String getParameterName() { ... }

    @Override
    public List<MediaType> resolveMediaTypes(ServerWebExchange exchange) { ... }
}

Usage:

import org.springframework.web.reactive.accept.ParameterContentTypeResolver;
import org.springframework.http.MediaType;
import java.util.Map;

Map<String, MediaType> mediaTypes = Map.of(
    "json", MediaType.APPLICATION_JSON,
    "xml", MediaType.APPLICATION_XML,
    "html", MediaType.TEXT_HTML
);

ParameterContentTypeResolver resolver = new ParameterContentTypeResolver(mediaTypes);
resolver.setParameterName("format");

// Example requests:
// GET /users?format=json -> [application/json]
// GET /users?format=xml -> [application/xml]
// GET /users?format=html -> [text/html]

API Versioning

API Version Resolution

The ApiVersionResolver interface extracts API version from requests.

@FunctionalInterface
public interface ApiVersionResolver {
    // Resolve API version from the request
    String resolveVersion(ServerWebExchange exchange);
}

Header API Version Resolver

The HeaderApiVersionResolver extracts version from a request header.

public class HeaderApiVersionResolver implements ApiVersionResolver {
    public static final String DEFAULT_HEADER_NAME = "API-Version";

    public HeaderApiVersionResolver() { ... }
    public HeaderApiVersionResolver(String headerName) { ... }

    // Set header name
    public void setHeaderName(String headerName) { ... }

    // Get header name
    public String getHeaderName() { ... }

    @Override
    public String resolveVersion(ServerWebExchange exchange) { ... }
}

Usage:

import org.springframework.web.reactive.accept.HeaderApiVersionResolver;

// Default header "API-Version"
ApiVersionResolver resolver = new HeaderApiVersionResolver();

// Custom header
ApiVersionResolver customResolver = new HeaderApiVersionResolver("X-API-Version");

// Example requests:
// GET /users
// API-Version: 1.0
// -> resolves to "1.0"

Query API Version Resolver

The QueryApiVersionResolver extracts version from a query parameter.

public class QueryApiVersionResolver implements ApiVersionResolver {
    public static final String DEFAULT_PARAMETER_NAME = "version";

    public QueryApiVersionResolver() { ... }
    public QueryApiVersionResolver(String parameterName) { ... }

    // Set parameter name
    public void setParameterName(String parameterName) { ... }

    // Get parameter name
    public String getParameterName() { ... }

    @Override
    public String resolveVersion(ServerWebExchange exchange) { ... }
}

Usage:

import org.springframework.web.reactive.accept.QueryApiVersionResolver;

// Default parameter "version"
ApiVersionResolver resolver = new QueryApiVersionResolver();

// Custom parameter
ApiVersionResolver customResolver = new QueryApiVersionResolver("api-version");

// Example requests:
// GET /users?version=1.0 -> resolves to "1.0"
// GET /users?api-version=2.0 -> resolves to "2.0"

Path API Version Resolver

The PathApiVersionResolver extracts version from the request path.

public class PathApiVersionResolver implements ApiVersionResolver {
    public PathApiVersionResolver(String pattern) { ... }

    // Get pattern
    public String getPattern() { ... }

    @Override
    public String resolveVersion(ServerWebExchange exchange) { ... }
}

Usage:

import org.springframework.web.reactive.accept.PathApiVersionResolver;

// Extract version from path segment
ApiVersionResolver resolver = new PathApiVersionResolver("/v{version}/**");

// Example requests:
// GET /v1/users -> resolves to "1"
// GET /v1.0/users -> resolves to "1.0"
// GET /v2/products -> resolves to "2"

// Different pattern
ApiVersionResolver resolver2 = new PathApiVersionResolver("/api/{version}/**");
// GET /api/1.0/users -> resolves to "1.0"

Media Type Parameter API Version Resolver

The MediaTypeParamApiVersionResolver extracts version from a media type parameter.

public class MediaTypeParamApiVersionResolver implements ApiVersionResolver {
    public static final String DEFAULT_PARAMETER_NAME = "v";

    public MediaTypeParamApiVersionResolver() { ... }
    public MediaTypeParamApiVersionResolver(String parameterName) { ... }

    // Set parameter name
    public void setParameterName(String parameterName) { ... }

    // Get parameter name
    public String getParameterName() { ... }

    @Override
    public String resolveVersion(ServerWebExchange exchange) { ... }
}

Usage:

import org.springframework.web.reactive.accept.MediaTypeParamApiVersionResolver;

// Default parameter "v"
ApiVersionResolver resolver = new MediaTypeParamApiVersionResolver();

// Custom parameter
ApiVersionResolver customResolver = new MediaTypeParamApiVersionResolver("version");

// Example requests:
// GET /users
// Accept: application/json;v=1.0
// -> resolves to "1.0"

// GET /users
// Accept: application/vnd.myapi+json;version=2.0
// -> resolves to "2.0"

API Version Strategy

The ApiVersionStrategy interface defines how versions are parsed and validated.

public interface ApiVersionStrategy {
    // Check if version string is valid
    boolean isValid(String version);

    // Check if version is deprecated
    boolean isDeprecated(String version);

    // Compare two versions
    int compare(String version1, String version2);

    // Get latest version
    String getLatestVersion();
}

Default API Version Strategy

The DefaultApiVersionStrategy supports fixed versions (e.g., "1.0") and baseline versions (e.g., "1.0+").

public class DefaultApiVersionStrategy implements ApiVersionStrategy {
    public DefaultApiVersionStrategy() { ... }

    // Add supported version
    public DefaultApiVersionStrategy addVersion(String version) { ... }

    // Add supported versions
    public DefaultApiVersionStrategy addVersions(String... versions) { ... }

    // Add deprecated version
    public DefaultApiVersionStrategy addDeprecatedVersion(String version) { ... }

    // Add deprecated versions
    public DefaultApiVersionStrategy addDeprecatedVersions(String... versions) { ... }

    // Set latest version
    public DefaultApiVersionStrategy setLatestVersion(String latestVersion) { ... }

    @Override
    public boolean isValid(String version) { ... }

    @Override
    public boolean isDeprecated(String version) { ... }

    @Override
    public int compare(String version1, String version2) { ... }

    @Override
    public String getLatestVersion() { ... }
}

Usage:

import org.springframework.web.reactive.accept.DefaultApiVersionStrategy;

DefaultApiVersionStrategy strategy = new DefaultApiVersionStrategy()
    .addVersions("1.0", "1.1", "2.0", "2.1")
    .addDeprecatedVersions("1.0", "1.1")
    .setLatestVersion("2.1");

// Check validity
strategy.isValid("2.0"); // true
strategy.isValid("3.0"); // false

// Check deprecation
strategy.isDeprecated("1.0"); // true
strategy.isDeprecated("2.0"); // false

// Compare versions
strategy.compare("1.0", "2.0"); // negative
strategy.compare("2.1", "2.0"); // positive
strategy.compare("2.0", "2.0"); // 0

// Baseline versions (match current and future versions)
strategy.addVersion("2.0+");
strategy.isValid("2.0"); // true
strategy.isValid("2.1"); // true
strategy.isValid("2.5"); // true
strategy.isValid("3.0"); // true
strategy.isValid("1.9"); // false

API Version Deprecation Handler

The ApiVersionDeprecationHandler interface handles deprecated API versions.

public interface ApiVersionDeprecationHandler {
    // Handle deprecated version
    void handleDeprecatedVersion(String version,
                                ServerWebExchange exchange);
}

Standard API Version Deprecation Handler

The StandardApiVersionDeprecationHandler adds deprecation warning headers to responses.

public class StandardApiVersionDeprecationHandler implements ApiVersionDeprecationHandler {
    public static final String DEFAULT_DEPRECATION_HEADER = "Deprecation";
    public static final String DEFAULT_SUNSET_HEADER = "Sunset";
    public static final String DEFAULT_LINK_HEADER = "Link";

    public StandardApiVersionDeprecationHandler() { ... }

    // Set deprecation date for version
    public void setDeprecationDate(String version, Instant deprecationDate) { ... }

    // Set sunset date for version
    public void setSunsetDate(String version, Instant sunsetDate) { ... }

    // Set documentation link for version
    public void setDocumentationLink(String version, String link) { ... }

    @Override
    public void handleDeprecatedVersion(String version, ServerWebExchange exchange) { ... }
}

Usage:

import org.springframework.web.reactive.accept.StandardApiVersionDeprecationHandler;
import java.time.Instant;

StandardApiVersionDeprecationHandler handler = new StandardApiVersionDeprecationHandler();

// Configure deprecation for version 1.0
handler.setDeprecationDate("1.0", Instant.parse("2024-01-01T00:00:00Z"));
handler.setSunsetDate("1.0", Instant.parse("2024-12-31T23:59:59Z"));
handler.setDocumentationLink("1.0", "https://api.example.com/docs/v1/migration");

// When handling a request with version 1.0, adds headers:
// Deprecation: @1704067200
// Sunset: Sun, 31 Dec 2024 23:59:59 GMT
// Link: <https://api.example.com/docs/v1/migration>; rel="deprecation"

API Version Configuration

Configure API versioning in WebFlux applications.

public class ApiVersionConfigurer {
    // Add version resolver
    public ApiVersionConfigurer addVersionResolver(ApiVersionResolver resolver) { ... }

    // Set version strategy
    public ApiVersionConfigurer setVersionStrategy(ApiVersionStrategy strategy) { ... }

    // Set deprecation handler
    public ApiVersionConfigurer setDeprecationHandler(ApiVersionDeprecationHandler handler) { ... }
}

Usage in configuration:

import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.accept.*;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.reactive.config.WebFluxConfigurer;

@Configuration
@EnableWebFlux
public class ApiVersioningConfig implements WebFluxConfigurer {

    @Override
    public void configureApiVersioning(ApiVersionConfigurer configurer) {
        // Add resolvers (header takes precedence)
        configurer
            .addVersionResolver(new HeaderApiVersionResolver("API-Version"))
            .addVersionResolver(new QueryApiVersionResolver("version"))
            .addVersionResolver(new PathApiVersionResolver("/v{version}/**"));

        // Configure version strategy
        DefaultApiVersionStrategy strategy = new DefaultApiVersionStrategy()
            .addVersions("1.0", "1.1", "2.0", "2.1", "3.0")
            .addDeprecatedVersions("1.0", "1.1")
            .setLatestVersion("3.0");
        configurer.setVersionStrategy(strategy);

        // Configure deprecation handler
        StandardApiVersionDeprecationHandler handler = new StandardApiVersionDeprecationHandler();
        handler.setDeprecationDate("1.0", Instant.parse("2023-01-01T00:00:00Z"));
        handler.setSunsetDate("1.0", Instant.parse("2024-12-31T23:59:59Z"));
        handler.setDocumentationLink("1.0", "https://api.example.com/docs/migration");
        configurer.setDeprecationHandler(handler);
    }
}

Using API Version in Controllers

Access resolved API version in controller methods:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.result.method.annotation.ApiVersionMethodArgumentResolver;
import reactor.core.publisher.Mono;

@RestController
@RequestMapping("/users")
public class VersionedUserController {

    // Version-specific endpoints using @RequestMapping
    @GetMapping(produces = "application/json;v=1.0")
    public Mono<UserV1> getUserV1() {
        return Mono.just(new UserV1());
    }

    @GetMapping(produces = "application/json;v=2.0")
    public Mono<UserV2> getUserV2() {
        return Mono.just(new UserV2());
    }

    // Access version as method parameter
    @GetMapping
    public Mono<String> getUser(@ApiVersion String version) {
        return Mono.just("User data for version: " + version);
    }
}

// Using RequestPredicates in functional routing
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;

@Configuration
public class VersionedRoutes {

    @Bean
    public RouterFunction<ServerResponse> routes(UserHandler handler) {
        return route()
            .GET("/users", version("1.0"), handler::handleV1)
            .GET("/users", version("2.0"), handler::handleV2)
            .build();
    }
}

Types

Version Request Condition

Request condition for API version matching in @RequestMapping.

public class VersionRequestCondition implements RequestCondition<VersionRequestCondition> {
    public VersionRequestCondition(String version) { ... }

    // Get version
    public String getVersion() { ... }

    @Override
    public VersionRequestCondition combine(VersionRequestCondition other) { ... }

    @Override
    public VersionRequestCondition getMatchingCondition(ServerWebExchange exchange) { ... }

    @Override
    public int compareTo(VersionRequestCondition other, ServerWebExchange exchange) { ... }
}

Complete Configuration Example

import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.accept.*;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.reactive.config.WebFluxConfigurer;

import java.time.Instant;
import java.util.Map;

@Configuration
@EnableWebFlux
public class CompleteNegotiationConfig implements WebFluxConfigurer {

    @Override
    public void configureContentTypeResolver(RequestedContentTypeResolverBuilder builder) {
        builder
            // Accept header resolution
            .headerResolver()
            // Query parameter resolution
            .parameterResolver("format")
            // Media type mappings
            .mediaType("json", MediaType.APPLICATION_JSON)
            .mediaType("xml", MediaType.APPLICATION_XML)
            .mediaType("html", MediaType.TEXT_HTML)
            .mediaTypes(Map.of(
                "pdf", MediaType.APPLICATION_PDF,
                "csv", MediaType.parseMediaType("text/csv")
            ))
            // Default content type
            .defaultContentTypes(MediaType.APPLICATION_JSON);
    }

    @Override
    public void configureApiVersioning(ApiVersionConfigurer configurer) {
        // Multiple resolution strategies
        configurer
            .addVersionResolver(new HeaderApiVersionResolver("API-Version"))
            .addVersionResolver(new QueryApiVersionResolver("version"))
            .addVersionResolver(new PathApiVersionResolver("/api/v{version}/**"))
            .addVersionResolver(new MediaTypeParamApiVersionResolver("v"));

        // Version strategy
        DefaultApiVersionStrategy strategy = new DefaultApiVersionStrategy()
            .addVersions("1.0", "1.1", "1.2", "2.0", "2.1", "3.0")
            .addDeprecatedVersions("1.0", "1.1")
            .setLatestVersion("3.0");
        configurer.setVersionStrategy(strategy);

        // Deprecation handling
        StandardApiVersionDeprecationHandler handler = new StandardApiVersionDeprecationHandler();
        handler.setDeprecationDate("1.0", Instant.parse("2023-01-01T00:00:00Z"));
        handler.setSunsetDate("1.0", Instant.parse("2024-06-30T23:59:59Z"));
        handler.setDocumentationLink("1.0", "https://api.example.com/docs/v1-migration");

        handler.setDeprecationDate("1.1", Instant.parse("2023-06-01T00:00:00Z"));
        handler.setSunsetDate("1.1", Instant.parse("2024-12-31T23:59:59Z"));
        handler.setDocumentationLink("1.1", "https://api.example.com/docs/v1.1-migration");

        configurer.setDeprecationHandler(handler);
    }
}