openapi-generator icon indicating copy to clipboard operation
openapi-generator copied to clipboard

[REQ] Jackson3 support for java libraries

Open paul-kraftlauget opened this issue 3 months ago • 6 comments

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.

paul-kraftlauget avatar Nov 06 '25 15:11 paul-kraftlauget

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);
    }
}

paul-kraftlauget avatar Nov 06 '25 15:11 paul-kraftlauget

Please note this dependency also: https://github.com/OpenAPITools/jackson-databind-nullable/issues/94

paul-kraftlauget avatar Nov 06 '25 15:11 paul-kraftlauget

@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?

holgerstolzenberg avatar Nov 24 '25 16:11 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...

holgerstolzenberg avatar Nov 24 '25 16:11 holgerstolzenberg

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"))
        }
    }
}

paul-kraftlauget avatar Nov 24 '25 18:11 paul-kraftlauget

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

detomarco avatar Dec 08 '25 08:12 detomarco