Skip to content

[REQ] Jackson3 support for java libraries #22294

Description

@paul-kraftlauget

Is your feature request related to a problem? Please describe.

Jackson 3 support for various java code generator.

Describe the solution you'd like

A configuration toggle in the java openapi generator to switch between jackson 2 or jackson 3.

For example, spring boot 4 deprecates jackson 2. So it migration is not necessarily needed...a user can continue to use jackson 2 even if migrating to spring boot 4.

Perhaps a configuration such as useJackson3 (like useJakartaEe)

Describe alternatives you've considered

I have used a custom template for the webclient library using an additionalProperty useJackson3.

Activity

  1. paul-kraftlauget commented on Nov 6, 2025

    @paul-kraftlauget
    ContributorAuthor

    Here is my ApiClient.mustache I have for my workaround until officially supported:

    {{>licenseInfo}}
    
    package {{invokerPackage}};
    
    {{#useJackson3}}
    import tools.jackson.core.JacksonException;
    import tools.jackson.databind.DeserializationFeature;
    import tools.jackson.databind.json.JsonMapper;
    {{/useJackson3}}
    {{^useJackson3}}
    import com.fasterxml.jackson.core.JsonProcessingException;
    import com.fasterxml.jackson.databind.DeserializationFeature;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
    {{/useJackson3}}
    {{#openApiNullable}}
    import org.openapitools.jackson.nullable.JsonNullableModule;
    {{/openApiNullable}}
    {{#generateClientAsBean}}
    import org.springframework.beans.factory.annotation.Autowired;
    {{/generateClientAsBean}}
    import org.springframework.core.ParameterizedTypeReference;
    import org.springframework.http.HttpHeaders;
    import org.springframework.http.HttpMethod;
    import org.springframework.http.HttpRequest;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.InvalidMediaTypeException;
    import org.springframework.http.MediaType;
    import org.springframework.http.RequestEntity;
    import org.springframework.http.RequestEntity.BodyBuilder;
    import org.springframework.http.ResponseEntity;
    import org.springframework.http.client.BufferingClientHttpRequestFactory;
    import org.springframework.http.client.ClientHttpRequestExecution;
    import org.springframework.http.client.ClientHttpRequestInterceptor;
    import org.springframework.http.client.ClientHttpResponse;
    import org.springframework.http.client.reactive.ClientHttpRequest;
    {{#useJackson3}}
    import org.springframework.http.codec.json.JacksonJsonDecoder;
    import org.springframework.http.codec.json.JacksonJsonEncoder;
    {{/useJackson3}}
    {{^useJackson3}}
    import org.springframework.http.codec.json.Jackson2JsonDecoder;
    import org.springframework.http.codec.json.Jackson2JsonEncoder;
    {{/useJackson3}}
    {{#generateClientAsBean}}
    import org.springframework.stereotype.Component;
    {{/generateClientAsBean}}
    import org.springframework.util.CollectionUtils;
    import org.springframework.util.LinkedMultiValueMap;
    import org.springframework.util.MultiValueMap;
    import org.springframework.util.StringUtils;
    import org.springframework.web.client.RestClientException;
    import org.springframework.web.util.UriComponentsBuilder;
    import org.springframework.web.reactive.function.client.WebClient;
    import org.springframework.web.reactive.function.client.WebClient.ResponseSpec;
    import org.springframework.web.reactive.function.client.ClientResponse;
    import org.springframework.web.reactive.function.BodyInserter;
    import org.springframework.web.reactive.function.BodyInserters;
    import org.springframework.web.reactive.function.client.ExchangeStrategies;
    import reactor.core.publisher.Mono;
    import reactor.core.publisher.Flux;
    import java.util.Optional;
    
    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.net.URLEncoder;
    import java.nio.charset.StandardCharsets;
    import java.text.DateFormat;
    import java.text.ParseException;
    import java.util.Arrays;
    import java.util.ArrayList;
    import java.util.Collection;
    import java.util.Collections;
    import java.util.Date;
    import java.util.HashMap;
    import java.util.Iterator;
    import java.util.List;
    import java.util.Map;
    import java.util.Map.Entry;
    import java.util.TimeZone;
    
    import {{javaxPackage}}.annotation.Nullable;
    
    {{#jsr310}}
    import java.time.OffsetDateTime;
    {{/jsr310}}
    
    import {{invokerPackage}}.auth.Authentication;
    import {{invokerPackage}}.auth.HttpBasicAuth;
    import {{invokerPackage}}.auth.HttpBearerAuth;
    import {{invokerPackage}}.auth.ApiKeyAuth;
    {{#hasOAuthMethods}}
    import {{invokerPackage}}.auth.OAuth;
    {{/hasOAuthMethods}}
    
    {{>generatedAnnotation}}
    
    {{#generateClientAsBean}}
    @Component("{{invokerPackage}}.ApiClient")
    {{/generateClientAsBean}}
    public class ApiClient{{#jsr310}} extends JavaTimeFormatter{{/jsr310}} {
        public enum CollectionFormat {
            CSV(","), TSV("\t"), SSV(" "), PIPES("|"), MULTI(null);
    
            protected final String separator;
            CollectionFormat(String separator) {
                this.separator = separator;
            }
    
            protected String collectionToString(Collection<?> collection) {
                return StringUtils.collectionToDelimitedString(collection, separator);
            }
        }
    
        protected static final String URI_TEMPLATE_ATTRIBUTE = WebClient.class.getName() + ".uriTemplate";
    
        protected HttpHeaders defaultHeaders = new HttpHeaders();
        protected MultiValueMap<String, String> defaultCookies = new LinkedMultiValueMap<String, String>();
    
        protected String basePath = "{{basePath}}";
    
        protected final WebClient webClient;
        protected final DateFormat dateFormat;
    {{#useJackson3}}
        protected final JsonMapper objectMapper;
    {{/useJackson3}}
    {{^useJackson3}}
        protected final ObjectMapper objectMapper;
    {{/useJackson3}}
        protected Map<String, Authentication> authentications;
    
    
        public ApiClient() {
            this.dateFormat = createDefaultDateFormat();
            this.objectMapper = createDefaultObjectMapper(this.dateFormat);
            this.webClient = buildWebClient(this.objectMapper);
            this.init();
        }
    
        {{#generateClientAsBean}}
        @Autowired
        {{/generateClientAsBean}}
        public ApiClient(WebClient webClient) {
            this(Optional.ofNullable(webClient).orElseGet(() -> buildWebClient()), createDefaultDateFormat());
        }
    
        public ApiClient({{#useJackson3}}JsonMapper{{/useJackson3}}{{^useJackson3}}ObjectMapper{{/useJackson3}} mapper, DateFormat format) {
                this(buildWebClient({{#useJackson3}}mapper{{/useJackson3}}{{^useJackson3}}mapper.copy(){{/useJackson3}}), format);
            }
    
        public ApiClient(WebClient webClient, {{#useJackson3}}JsonMapper{{/useJackson3}}{{^useJackson3}}ObjectMapper{{/useJackson3}} mapper, DateFormat format) {
            this(Optional.ofNullable(webClient).orElseGet(() -> buildWebClient({{#useJackson3}}mapper{{/useJackson3}}{{^useJackson3}}mapper.copy(){{/useJackson3}})), format);
        }
    
        protected ApiClient(WebClient webClient, DateFormat format) {
            this.webClient = webClient;
            this.dateFormat = format;
            this.objectMapper = createDefaultObjectMapper(format);
            this.init();
        }
    
        public static DateFormat createDefaultDateFormat() {
            DateFormat dateFormat = new RFC3339DateFormat();
            dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
            return dateFormat;
        }
        public static {{#useJackson3}}JsonMapper{{/useJackson3}}{{^useJackson3}}ObjectMapper{{/useJackson3}} createDefaultObjectMapper(@Nullable DateFormat dateFormat) {
            if (null == dateFormat) {
                dateFormat = createDefaultDateFormat();
            }
            {{#useJackson3}}
            JsonMapper mapper = JsonMapper.builder()
                .defaultDateFormat(dateFormat)
                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, {{failOnUnknownProperties}})
                {{#openApiNullable}}
                // FIXME use this when useJackson3 is supported: .addModule(new JsonNullableModule())
                {{/openApiNullable}}
                .build();
    
            {{/useJackson3}}
            {{^useJackson3}}
            ObjectMapper mapper = new ObjectMapper();
                mapper.setDateFormat(dateFormat);
                mapper.registerModule(new JavaTimeModule());
                mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, {{failOnUnknownProperties}});
                {{#openApiNullable}}
                JsonNullableModule jnm = new JsonNullableModule();
                mapper.registerModule(jnm);
                {{/openApiNullable}}
            {{/useJackson3}}
            return mapper;
        }
    
        protected void init() {
            // Setup authentications (key: authentication name, value: authentication).
            authentications = new HashMap<String, Authentication>();{{#authMethods}}{{#isBasic}}{{#isBasicBasic}}
            authentications.put("{{name}}", new HttpBasicAuth());{{/isBasicBasic}}{{#isBasicBearer}}
            authentications.put("{{name}}", new HttpBearerAuth("{{scheme}}"));{{/isBasicBearer}}{{/isBasic}}{{#isApiKey}}
            authentications.put("{{name}}", new ApiKeyAuth({{#isKeyInHeader}}"header"{{/isKeyInHeader}}{{#isKeyInQuery}}"query"{{/isKeyInQuery}}{{#isKeyInCookie}}"cookie"{{/isKeyInCookie}}, "{{keyParamName}}"));{{/isApiKey}}{{#isOAuth}}
            authentications.put("{{name}}", new OAuth());{{/isOAuth}}{{/authMethods}}
            // Prevent the authentications from being modified.
            authentications = Collections.unmodifiableMap(authentications);
        }
    
        /**
        * Build the WebClientBuilder used to make WebClient.
        * @param mapper ObjectMapper used for serialize/deserialize
        * @return WebClient
        */
        public static WebClient.Builder buildWebClientBuilder({{#useJackson3}}JsonMapper{{/useJackson3}}{{^useJackson3}}ObjectMapper{{/useJackson3}} mapper) {
            ExchangeStrategies strategies = ExchangeStrategies
                .builder()
                .codecs(clientDefaultCodecsConfigurer -> {
                    {{#useJackson3}}
                    clientDefaultCodecsConfigurer.defaultCodecs().jacksonJsonEncoder(new JacksonJsonEncoder(mapper));
                    clientDefaultCodecsConfigurer.defaultCodecs().jacksonJsonDecoder(new JacksonJsonDecoder(mapper));
                    {{/useJackson3}}
                    {{^useJackson3}}
                    clientDefaultCodecsConfigurer.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(mapper, MediaType.APPLICATION_JSON));
                    clientDefaultCodecsConfigurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(mapper, MediaType.APPLICATION_JSON));
                    {{/useJackson3}}
                }).build();
            WebClient.Builder webClientBuilder = WebClient.builder().exchangeStrategies(strategies);
            return webClientBuilder;
        }
    
        /**
         * Build the WebClientBuilder used to make WebClient.
         * @return WebClient
         */
        public static WebClient.Builder buildWebClientBuilder() {
            return buildWebClientBuilder(createDefaultObjectMapper(null));
        }
    
        /**
         * Build the WebClient used to make HTTP requests.
         * @param mapper {{#useJackson3}}JsonMapper{{/useJackson3}}{{^useJackson3}}ObjectMapper{{/useJackson3}} used for serialize/deserialize
         * @return WebClient
         */
        public static WebClient buildWebClient({{#useJackson3}}JsonMapper{{/useJackson3}}{{^useJackson3}}ObjectMapper{{/useJackson3}} mapper) {
            return buildWebClientBuilder(mapper).build();
        }
    
        /**
         * Build the WebClient used to make HTTP requests.
         * @return WebClient
         */
        public static WebClient buildWebClient() {
            return buildWebClientBuilder(createDefaultObjectMapper(null)).build();
        }
    
        /**
         * Get the current base path
         * @return String the base path
         */
        public String getBasePath() {
            return basePath;
        }
    
        /**
         * Set the base path, which should include the host
         * @param basePath the base path
         * @return ApiClient this client
         */
        public ApiClient setBasePath(String basePath) {
            this.basePath = basePath;
            return this;
        }
    
        /**
         * Get authentications (key: authentication name, value: authentication).
         * @return Map the currently configured authentication types
         */
        public Map<String, Authentication> getAuthentications() {
            return authentications;
        }
    
        /**
         * Get authentication for the given name.
         *
         * @param authName The authentication name
         * @return The authentication, null if not found
         */
        public Authentication getAuthentication(String authName) {
            return authentications.get(authName);
        }
    
        /**
         * Helper method to set access token for the first Bearer authentication.
         * @param bearerToken Bearer token
         */
        public void setBearerToken(String bearerToken) {
            for (Authentication auth : authentications.values()) {
                if (auth instanceof HttpBearerAuth) {
                    ((HttpBearerAuth) auth).setBearerToken(bearerToken);
                    return;
                }
            }
            throw new RuntimeException("No Bearer authentication configured!");
        }
    
        /**
         * Helper method to set username for the first HTTP basic authentication.
         * @param username the username
         */
        public void setUsername(String username) {
            for (Authentication auth : authentications.values()) {
                if (auth instanceof HttpBasicAuth) {
                    ((HttpBasicAuth) auth).setUsername(username);
                    return;
                }
            }
            throw new RuntimeException("No HTTP basic authentication configured!");
        }
    
        /**
         * Helper method to set password for the first HTTP basic authentication.
         * @param password the password
         */
        public void setPassword(String password) {
            for (Authentication auth : authentications.values()) {
                if (auth instanceof HttpBasicAuth) {
                    ((HttpBasicAuth) auth).setPassword(password);
                    return;
                }
            }
            throw new RuntimeException("No HTTP basic authentication configured!");
        }
    
        /**
         * Helper method to set API key value for the first API key authentication.
         * @param apiKey the API key
         */
        public void setApiKey(String apiKey) {
            for (Authentication auth : authentications.values()) {
                if (auth instanceof ApiKeyAuth) {
                    ((ApiKeyAuth) auth).setApiKey(apiKey);
                    return;
                }
            }
            throw new RuntimeException("No API key authentication configured!");
        }
    
        /**
         * Helper method to set API key prefix for the first API key authentication.
         * @param apiKeyPrefix the API key prefix
         */
        public void setApiKeyPrefix(String apiKeyPrefix) {
            for (Authentication auth : authentications.values()) {
                if (auth instanceof ApiKeyAuth) {
                    ((ApiKeyAuth) auth).setApiKeyPrefix(apiKeyPrefix);
                    return;
                }
            }
            throw new RuntimeException("No API key authentication configured!");
        }
    
        {{#hasOAuthMethods}}
        /**
         * Helper method to set access token for the first OAuth2 authentication.
         * @param accessToken the access token
         */
        public void setAccessToken(String accessToken) {
            for (Authentication auth : authentications.values()) {
                if (auth instanceof OAuth) {
                    ((OAuth) auth).setAccessToken(accessToken);
                    return;
                }
            }
            throw new RuntimeException("No OAuth2 authentication configured!");
        }
    
        {{/hasOAuthMethods}}
        /**
         * Set the User-Agent header's value (by adding to the default header map).
         * @param userAgent the user agent string
         * @return ApiClient this client
         */
        public ApiClient setUserAgent(String userAgent) {
            addDefaultHeader("User-Agent", userAgent);
            return this;
        }
    
        /**
         * Add a default header.
         *
         * @param name The header's name
         * @param value The header's value
         * @return ApiClient this client
         */
        public ApiClient addDefaultHeader(String name, String value) {
            defaultHeaders.set(name, value);
            return this;
        }
    
        /**
         * Add a default cookie.
         *
         * @param name The cookie's name
         * @param value The cookie's value
         * @return ApiClient this client
         */
        public ApiClient addDefaultCookie(String name, String value) {
            if (defaultCookies.containsKey(name)) {
                defaultCookies.remove(name);
            }
            defaultCookies.add(name, value);
            return this;
        }
    
        /**
         * Get the date format used to parse/format date parameters.
         * @return DateFormat format
         */
        public DateFormat getDateFormat() {
            return dateFormat;
        }
    
        /**
         * Parse the given string into Date object.
         */
        public Date parseDate(String str) {
            try {
                return dateFormat.parse(str);
            } catch (ParseException e) {
                throw new RuntimeException(e);
            }
        }
    
        /**
         * Format the given Date object into string.
         */
        public String formatDate(Date date) {
            return dateFormat.format(date);
        }
    
        /**
         * Get the {{#useJackson3}}JsonMapper{{/useJackson3}}{{^useJackson3}}ObjectMapper{{/useJackson3}} used to make HTTP requests.
         * @return {{#useJackson3}}JsonMapper{{/useJackson3}}{{^useJackson3}}ObjectMapper{{/useJackson3}} objectMapper
         */
        public {{#useJackson3}}JsonMapper{{/useJackson3}}{{^useJackson3}}ObjectMapper{{/useJackson3}} getObjectMapper() {
            return objectMapper;
        }
    
        /**
         * Get the WebClient used to make HTTP requests.
         * @return WebClient webClient
         */
        public WebClient getWebClient() {
            return webClient;
        }
    
        /**
         * Format the given parameter object into string.
         * @param param the object to convert
         * @return String the parameter represented as a String
         */
        public String parameterToString(Object param) {
            if (param == null) {
                return "";
            } else if (param instanceof Date) {
                return formatDate( (Date) param);
            } {{#jsr310}}else if (param instanceof OffsetDateTime) {
                return formatOffsetDateTime((OffsetDateTime) param);
            } {{/jsr310}}else if (param instanceof Collection) {
                StringBuilder b = new StringBuilder();
                for(Object o : (Collection<?>) param) {
                    if(b.length() > 0) {
                        b.append(",");
                    }
                    b.append(String.valueOf(o));
                }
                return b.toString();
            } else {
                return String.valueOf(param);
            }
        }
    
        /**
        * Converts a parameter to a {@link MultiValueMap} containing Json-serialized values for use in REST requests
        * @param collectionFormat The format to convert to
        * @param name The name of the parameter
        * @param value The parameter's value
        * @return a Map containing the Json-serialized String value(s) of the input parameter
        */
        public MultiValueMap<String, String> parameterToMultiValueMapJson(CollectionFormat collectionFormat, String name, Object value) {
            Collection<?> valueCollection;
            if (value instanceof Collection) {
                valueCollection = (Collection<?>) value;
            } else {
                try {
                    return parameterToMultiValueMap(collectionFormat, name, objectMapper.writeValueAsString(value));
                } catch ({{#useJackson3}}JacksonException{{/useJackson3}}{{^useJackson3}}JsonProcessingException{{/useJackson3}} e) {
                    throw new RuntimeException(e);
                }
            }
    
            List<String> values = new ArrayList<>();
            for(Object o : valueCollection) {
                try {
                    values.add(objectMapper.writeValueAsString(o));
                } catch ({{#useJackson3}}JacksonException{{/useJackson3}}{{^useJackson3}}JsonProcessingException{{/useJackson3}} e) {
                    throw new RuntimeException(e);
                }
            }
            return parameterToMultiValueMap(collectionFormat, name, "[" + StringUtils.collectionToDelimitedString(values, collectionFormat.separator) + "]");
        }
    
        /**
         * Converts a parameter to a {@link MultiValueMap} for use in REST requests
         * @param collectionFormat The format to convert to
         * @param name The name of the parameter
         * @param value The parameter's value
         * @return a Map containing the String value(s) of the input parameter
         */
        public MultiValueMap<String, String> parameterToMultiValueMap(CollectionFormat collectionFormat, String name, Object value) {
            final MultiValueMap<String, String> params = new LinkedMultiValueMap<String, String>();
    
            if (name == null || name.isEmpty() || value == null) {
                return params;
            }
    
            if(collectionFormat == null) {
                collectionFormat = CollectionFormat.CSV;
            }
    
            if (value instanceof Map) {
                @SuppressWarnings("unchecked")
                final Map<String, Object> valuesMap = (Map<String, Object>) value;
                for (final Entry<String, Object> entry : valuesMap.entrySet()) {
                    params.add(entry.getKey(), parameterToString(entry.getValue()));
                }
                return params;
            }
    
            Collection<?> valueCollection = null;
            if (value instanceof Collection) {
                valueCollection = (Collection<?>) value;
            } else {
                params.add(name, parameterToString(value));
                return params;
            }
    
            if (valueCollection.isEmpty()){
                return params;
            }
    
            if (collectionFormat.equals(CollectionFormat.MULTI)) {
                for (Object item : valueCollection) {
                    params.add(name, parameterToString(item));
                }
                return params;
            }
    
            List<String> values = new ArrayList<String>();
            for(Object o : valueCollection) {
                values.add(parameterToString(o));
            }
            params.add(name, collectionFormat.collectionToString(values));
    
            return params;
        }
    
        /**
        * Check if the given {@code String} is a JSON MIME.
        * @param mediaType the input MediaType
        * @return boolean true if the MediaType represents JSON, false otherwise
        */
        public boolean isJsonMime(String mediaType) {
            // "* / *" is default to JSON
            if ("*/*".equals(mediaType)) {
                return true;
            }
    
            try {
                return isJsonMime(MediaType.parseMediaType(mediaType));
            } catch (InvalidMediaTypeException e) {
            }
            return false;
        }
    
        /**
         * Check if the given MIME is a JSON MIME.
         * JSON MIME examples:
         *     application/json
         *     application/json; charset=UTF8
         *     APPLICATION/JSON
         * @param mediaType the input MediaType
         * @return boolean true if the MediaType represents JSON, false otherwise
         */
        public boolean isJsonMime(MediaType mediaType) {
            return mediaType != null && (MediaType.APPLICATION_JSON.isCompatibleWith(mediaType) || mediaType.getSubtype().matches("^.*(\\+json|ndjson)[;]?\\s*$"));
        }
    
        /**
        * Check if the given {@code String} is a Problem JSON MIME (RFC-7807).
        * @param mediaType the input MediaType
        * @return boolean true if the MediaType represents Problem JSON, false otherwise
        */
        public boolean isProblemJsonMime(String mediaType) {
            return "application/problem+json".equalsIgnoreCase(mediaType);
        }
    
        /**
         * Select the Accept header's value from the given accepts array:
         *     if JSON exists in the given array, use it;
         *     otherwise use all of them (joining into a string)
         *
         * @param accepts The accepts array to select from
         * @return List The list of MediaTypes to use for the Accept header
         */
        public List<MediaType> selectHeaderAccept(String[] accepts) {
            if (accepts.length == 0) {
                return null;
            }
            for (String accept : accepts) {
                MediaType mediaType = MediaType.parseMediaType(accept);
                if (isJsonMime(mediaType) && !isProblemJsonMime(accept)) {
                    return Collections.singletonList(mediaType);
                }
            }
            return MediaType.parseMediaTypes(StringUtils.arrayToCommaDelimitedString(accepts));
        }
    
        /**
         * Select the Content-Type header's value from the given array:
         *     if JSON exists in the given array, use it;
         *     otherwise use the first one of the array.
         *
         * @param contentTypes The Content-Type array to select from
         * @return MediaType The Content-Type header to use. If the given array is empty, null will be returned.
         */
        public MediaType selectHeaderContentType(String[] contentTypes) {
            if (contentTypes.length == 0) {
                return null;
            }
            for (String contentType : contentTypes) {
                MediaType mediaType = MediaType.parseMediaType(contentType);
                if (isJsonMime(mediaType)) {
                    return mediaType;
                }
            }
            return MediaType.parseMediaType(contentTypes[0]);
        }
    
        /**
         * Select the body to use for the request
         * @param obj the body object
         * @param formParams the form parameters
         * @param contentType the content type of the request
         * @return Object the selected body
         */
        protected BodyInserter<?, ? super ClientHttpRequest> selectBody(Object obj, MultiValueMap<String, Object> formParams, MediaType contentType) {
            if(MediaType.APPLICATION_FORM_URLENCODED.equals(contentType)) {
                MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
    
                formParams
                        .toSingleValueMap()
                        .entrySet()
                        .forEach(es -> map.add(es.getKey(), String.valueOf(es.getValue())));
    
                return BodyInserters.fromFormData(map);
            } else if(MediaType.MULTIPART_FORM_DATA.equals(contentType)) {
                return BodyInserters.fromMultipartData(formParams);
            } else {
                return obj != null ? BodyInserters.fromValue(obj) : null;
            }
        }
    
        /**
         * Invoke API by sending HTTP request with the given options.
         *
         * @param <T> the return type to use
         * @param path The sub-path of the HTTP URL
         * @param method The request method
         * @param pathParams The path parameters
         * @param queryParams The query parameters
         * @param body The request body object
         * @param headerParams The header parameters
         * @param formParams The form parameters
         * @param accept The request's Accept header
         * @param contentType The request's Content-Type header
         * @param authNames The authentications to apply
         * @param returnType The return type into which to deserialize the response
         * @return The response body in chosen type
         */
        public <T> ResponseSpec invokeAPI(String path, HttpMethod method, Map<String, Object> pathParams, MultiValueMap<String, String> queryParams, Object body, HttpHeaders headerParams, MultiValueMap<String, String> cookieParams, MultiValueMap<String, Object> formParams, List<MediaType> accept, MediaType contentType, String[] authNames, ParameterizedTypeReference<T> returnType) throws RestClientException {
            final WebClient.RequestBodySpec requestBuilder = prepareRequest(path, method, pathParams, queryParams, body, headerParams, cookieParams, formParams, accept, contentType, authNames);
            return requestBuilder.retrieve();
        }
    
        /**
         * Include queryParams in uriParams taking into account the paramName
         * @param queryParams The query parameters
         * @param uriParams The path parameters
         * return templatized query string
         */
        protected String generateQueryUri(MultiValueMap<String, String> queryParams, Map<String, Object> uriParams) {
            StringBuilder queryBuilder = new StringBuilder();
            queryParams.forEach((name, values) -> {
                if (CollectionUtils.isEmpty(values)) {
                    if (queryBuilder.length() != 0) {
                        queryBuilder.append('&');
                    }
                    queryBuilder.append(name);
                } else {
                    int valueItemCounter = 0;
                    for (Object value : values) {
                        if (queryBuilder.length() != 0) {
                            queryBuilder.append('&');
                        }
                        queryBuilder.append(name);
                        if (value != null) {
                            String templatizedKey = name + valueItemCounter++;
                            uriParams.put(templatizedKey, value.toString());
                            queryBuilder.append('=').append("{").append(templatizedKey).append("}");
                        }
                    }
                }
            });
            return queryBuilder.toString();
        }
    
        protected WebClient.RequestBodySpec prepareRequest(String path, HttpMethod method, Map<String, Object> pathParams,
            MultiValueMap<String, String> queryParams, Object body, HttpHeaders headerParams,
            MultiValueMap<String, String> cookieParams, MultiValueMap<String, Object> formParams, List<MediaType> accept,
            MediaType contentType, String[] authNames) {
            updateParamsForAuth(authNames, queryParams, headerParams, cookieParams);
    
            final UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(basePath).path(path);
    
            String finalUri = builder.build(false).toUriString();
            Map<String, Object> uriParams = new HashMap<>();
            uriParams.putAll(pathParams);
    
            if (queryParams != null && !queryParams.isEmpty()) {
                //Include queryParams in uriParams taking into account the paramName
                String queryUri = generateQueryUri(queryParams, uriParams);
                //Append to finalUri the templatized query string like "?param1={param1Value}&.......
                finalUri += "?" + queryUri;
            }
    
            final WebClient.RequestBodySpec requestBuilder = webClient.method(method).uri(finalUri, uriParams);
    
            if (accept != null) {
                requestBuilder.accept(accept.toArray(new MediaType[accept.size()]));
            }
            if(contentType != null) {
                requestBuilder.contentType(contentType);
            }
    
            addHeadersToRequest(headerParams, requestBuilder);
            addHeadersToRequest(defaultHeaders, requestBuilder);
            addCookiesToRequest(cookieParams, requestBuilder);
            addCookiesToRequest(defaultCookies, requestBuilder);
    
            requestBuilder.attribute(URI_TEMPLATE_ATTRIBUTE, path);
    
            requestBuilder.body(selectBody(body, formParams, contentType));
            return requestBuilder;
        }
    
        /**
         * Add headers to the request that is being built
         * @param headers The headers to add
         * @param requestBuilder The current request
         */
        protected void addHeadersToRequest(HttpHeaders headers, WebClient.RequestBodySpec requestBuilder) {
        {{#useJakartaEe}}
            for (Entry<String, List<String>> entry : headers.headerSet()) {
        {{/useJakartaEe}}
        {{^useJakartaEe}}
            for (Entry<String, List<String>> entry : headers.entrySet()) {
        {{/useJakartaEe}}
                List<String> values = entry.getValue();
                for(String value : values) {
                    if (value != null) {
                        requestBuilder.header(entry.getKey(), value);
                    }
                }
            }
        }
    
        /**
         * Add cookies to the request that is being built
         * @param cookies The cookies to add
         * @param requestBuilder The current request
         */
        protected void addCookiesToRequest(MultiValueMap<String, String> cookies, WebClient.RequestBodySpec requestBuilder) {
            for (Entry<String, List<String>> entry : cookies.entrySet()) {
                List<String> values = entry.getValue();
                for(String value : values) {
                    if (value != null) {
                        requestBuilder.cookie(entry.getKey(), value);
                    }
                }
            }
        }
    
        /**
         * Update query and header parameters based on authentication settings.
         *
         * @param authNames The authentications to apply
         * @param queryParams The query parameters
         * @param headerParams The header parameters
         * @param cookieParams the cookie parameters
         */
        protected void updateParamsForAuth(String[] authNames, MultiValueMap<String, String> queryParams, HttpHeaders headerParams, MultiValueMap<String, String> cookieParams) {
            for (String authName : authNames) {
                Authentication auth = authentications.get(authName);
                if (auth == null) {
                    throw new RestClientException("Authentication undefined: " + authName);
                }
                auth.applyToParams(queryParams, headerParams, cookieParams);
            }
        }
    
        /**
        * Formats the specified collection path parameter to a string value.
        *
        * @param collectionFormat The collection format of the parameter.
        * @param values The values of the parameter.
        * @return String representation of the parameter
        */
        public String collectionPathParameterToString(CollectionFormat collectionFormat, Collection<?> values) {
            // create the value based on the collection format
            if (CollectionFormat.MULTI.equals(collectionFormat)) {
                // not valid for path params
                return parameterToString(values);
            }
    
             // collectionFormat is assumed to be "csv" by default
            if(collectionFormat == null) {
                collectionFormat = CollectionFormat.CSV;
            }
    
            return collectionFormat.collectionToString(values);
        }
    }
    
  2. paul-kraftlauget commented on Nov 6, 2025

    @paul-kraftlauget
    ContributorAuthor

    Please note this dependency also: OpenAPITools/jackson-databind-nullable#94

  3. holgerstolzenberg commented on Nov 24, 2025

    @holgerstolzenberg

    @paul-kraftlauget

    Thanks for your mustache example! Yet I am struggeling to apply it properly. For me the additional property useJackson3 does not evaluate.

    Would you be willing to share your full openapi-generator-maven-plugin config?

  4. holgerstolzenberg commented on Nov 24, 2025

    @holgerstolzenberg

    Another note to boost the issue: recently released Spring Boot v4 has deprecated Jackson2 in favour of v3. Even if a compatibility mode exits, a mix of both Jackson versions on the classpath for sure will cause issues here and there...

  5. paul-kraftlauget commented on Nov 24, 2025

    @paul-kraftlauget
    ContributorAuthor

    I had to set it with "additionalProperties", not "configOptions" to get it to work with a customTemplate.

    1. Copy and paste the above code to ./src/main/resources/custom_template/ApiClient.mustache.
    2. build.gradle.kts:
    val clientApis = listOf(
        "messaging-v1"
    )
    
    clientApis.forEach {
        val fileName = "${it}_openapi.yaml"
    
        tasks.register<org.openapitools.generator.gradle.plugin.tasks.ValidateTask>("${it}-validate") {
            group = "openapi tools"
            description = "Validates $fileName is according to spec"
            inputSpec.set(project.file("src/main/resources/apis/${fileName}").absolutePath)
            recommend.set(true)
        }
    
        val packageName = "com.company.client.api.${it}".replace("-", ".")
        tasks.register<org.openapitools.generator.gradle.plugin.tasks.GenerateTask>("${it}-generate") {
            group = "openapi tools"
            description = "Generates the code from $fileName"
            generatorName.set("java")
            library.set("webclient")
            inputSpec.set(project.file("src/main/resources/apis/${fileName}").absolutePath)
            outputDir.set(layout.buildDirectory.dir("generated/sources/openapi").map { dir -> dir.asFile.path })
    
            apiPackage.set("${packageName}.api")
            invokerPackage.set("${packageName}.invoker")
            modelPackage.set("${packageName}.model")
            templateResourcePath.set("$projectDir/src/main/resources/custom_template") // to use Jackson3 until openapi generator supports it
            generateApiTests.set(false)
            generateModelTests.set(false)
            configOptions.set(
                mapOf(
                    "dateLibrary" to "java8",
                    "useBeanValidation" to "false",
                    "performBeanValidation" to "true",
                    "useJakartaEe" to "true",
                    "interfaceOnly" to "true",
                    "openApiNullable" to "false",
                )
            )
            additionalProperties.set(
                mapOf(
                    "useJackson3" to true // or false
                )
            )
        }
    
        tasks.withType<JavaCompile> {
            dependsOn("${it}-validate")
            dependsOn("${it}-generate")
        }
    }
    
    sourceSets {
        main {
            java {
                srcDir(layout.buildDirectory.dir("generated/sources/openapi/src/main/java"))
            }
        }
    }
    
  6. detomarco commented on Dec 8, 2025

    @detomarco
    Contributor

    Is it possible to extend the request in order to add jackson 3 support to kotlin libray as well?

  7. desperateCoder commented on Jan 22, 2026

    @desperateCoder

    +1

    Consider using useSpringBoot4 as a key, since it correlates to useSpringBoot3 in the configOptions

  8. antechrestos commented on Jan 29, 2026

    @antechrestos
    Contributor

    I think that useJackson3 and useSpringBoot4 needs to remain separated.

    • jackson is used in quite a lot templates out of spring generator's ones. So a useJackson3 may be used
    • for spring boot 4: class have not much be moved (RestClient, RestController ) (despite starters, auto conf) which is without impact on class generated. This however has an impact on pom generated (there is a pom-sb3.mustache in the spring-boot librarie under JavaSpring)

    as spotted by @detomarco , the impact for adding jackson 3 to templates is a heavy work . As an example, I searched the import com.fasterxml.jackson string in all the mustache files of the project in 7.19.0 and I counted 131 files.

    This does not mean that each file will be impacted, yet it will need to review each file to check whether it imports resources that are still up to date in jackson 3 or need some adaptation.

  9. rupert-jung-mw commented on Jan 29, 2026

    @rupert-jung-mw

    Maybe just use separate keys for client (RestClient vs. RestTemplate) and JacksonVersion (2 vs. 3)

  10. aitkarra commented on Jan 30, 2026

    @aitkarra

    Hello,
    Is an official solution being considered?
    Support for Spring Boot 3.5.x is planned until June 2026 (https://spring.io/projects/spring-boot#support).

  11. ramondebeijer commented on Jan 30, 2026

    @ramondebeijer

    @paul-kraftlauget when I try the example above I get an error in generated api clients because there is a mismatch between the ApiClient class importing import org.springframework.web.reactive.function.client.WebClient.ResponseSpec; while the class using the ApiClient uses org.springframework.web.client.ResponseSpec don't you have that issue?

  12. paul-kraftlauget commented on Jan 30, 2026

    @paul-kraftlauget
    ContributorAuthor

    @paul-kraftlauget when I try the example above I get an error in generated api clients because there is a mismatch between the ApiClient class importing import org.springframework.web.reactive.function.client.WebClient.ResponseSpec; while the class using the ApiClient uses org.springframework.web.client.ResponseSpec don't you have that issue?

    When I first posted this issue and the example code, it was for Spring Boot 4.0.0-M3 (milestone 3). Spring changed a few package names for their final release.

  13. antechrestos commented on Jan 30, 2026

    @antechrestos
    Contributor

    I am about to push a PR for spring generator

  14. 46 remaining items

  15. added a commit that references this issue on Mar 5, 2026
  16. yonatankarp commented on Mar 6, 2026

    @yonatankarp
    Contributor

    It ended up a magnificent beast, but I guess that it's expected since SpringBoot 4 was not set for Kotlin at all: #23154

    I will start working on another one for the kotlin client generator, which I hope(!) will not be as big.

  17. muchtar5 commented on Mar 9, 2026

    @muchtar5

    Please use the same key as they are used for the spring server and also consider implementing useSpringBoot4 and useJackson3 for kotlin client.

  18. yonatankarp commented on Mar 9, 2026

    @yonatankarp
    Contributor

    @muchtar5 I will, just in a seperate pr, it's already a bit out of control with 3.5k lines changed

  19. muchtar5 commented on Mar 9, 2026

    @muchtar5

    @muchtar5 I will, just in a seperate pr, it's already a bit out of control with 3.5k lines changed

    thanks, could you please link the PR here, that would help me and future requests

  20. yonatankarp commented on Mar 9, 2026

    @yonatankarp
    Contributor

    Yap:

    I still need to fix some comments by AI on the PR before I move it to ready for review, but I'm almost there 😄

  21. stil4m commented on Mar 10, 2026

    @stil4m

    I may have an alternative that may fit the issues some of us are facing. This here: https://github.com/Stijlaart-IT/spring-oas-generator.

    It provides integration with Spring via HTTP Service Clients instead of wrapper classes, and supports Java record syntax instead of classes.
    It is solely focuses on Spring, and aims to focus on seamless integration and support the latest versions of Spring (Boot).

    It might not suit everyone, but might be worth a look ;)

  22. added a commit that references this issue on Mar 13, 2026
    300e585
  23. ann94a commented on Apr 9, 2026

    @ann94a

    Hello,
    Will you support the library feign?
    Best regards

  24. torstenwerner commented on Apr 12, 2026

    @torstenwerner

    Hello, Will you support the library feign? Best regards

    Why do you comment an issue related to Jackson3? Please read the documentation at https://openapi-generator.tech/docs/generators/java/ . It mentions feign.

  25. added a commit that references this issue on Apr 27, 2026
    65a51d0
  26. christian-ertl commented on Jul 10, 2026

    @christian-ertl

    Hello, Will you support the library feign? Best regards

    Why do you comment an issue related to Jackson3? Please read the documentation at https://openapi-generator.tech/docs/generators/java/ . It mentions feign.

    I'd assume the reason is that Feign can also use Jackson instead of Gson (mentioned in the linked documentation). So will there be support for Feign and Jackson 3? Feign itself does support it, but the generated classes refer to the Feign classes for Jackson 2.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions