You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
Reacted by Nicholas Rayburn, Maximilian Laue, haeuserd, Alexander Kose, cetrimble, osprscheel, Joshua, marekSeda, ebrjens, oscarobr and 94 more
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);
}
}
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...
Reacted by cetrimble, mriesen-sbb, Yamahar, Mats Stijlaart, tde-desc, David J. M. Karlsen, Peter Hicks, aleksey-astashenko-jemmic, wyfrel, Martin Timko and 8 more
I had to set it with "additionalProperties", not "configOptions" to get it to work with a customTemplate.
Copy and paste the above code to ./src/main/resources/custom_template/ApiClient.mustache.
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"))
}
}
}
Is it possible to extend the request in order to add jackson 3 support to kotlin libray as well?
Reacted by Peter Storch, Hannes Kohlsaat, YesperP, Yamahar, marekSeda, aleksey-astashenko-jemmic, Donát Csongor, Maximilien Riehl, Paweł Wysocki, Martin Timko and 16 more
Consider using useSpringBoot4 as a key, since it correlates to useSpringBoot3 in the configOptions
Reacted by Sourabh Sharma, rupert-jung-mw, aleksey-astashenko-jemmic, Renato Mameli, yamaguchikazuyuki, sbknch, Michael Vasilenko, Thomas Bosch, Kari Salonen, Shaun Jackson and 1 more
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.
@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?
@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.
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 ;)
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.
Activity
paul-kraftlauget commentedon Nov 6, 2025
Here is my ApiClient.mustache I have for my workaround until officially supported:
paul-kraftlauget commentedon Nov 6, 2025
Please note this dependency also: OpenAPITools/jackson-databind-nullable#94
holgerstolzenberg commentedon Nov 24, 2025
@paul-kraftlauget
Thanks for your mustache example! Yet I am struggeling to apply it properly. For me the additional property
useJackson3does not evaluate.Would you be willing to share your full openapi-generator-maven-plugin config?
holgerstolzenberg commentedon Nov 24, 2025
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...
paul-kraftlauget commentedon Nov 24, 2025
I had to set it with "additionalProperties", not "configOptions" to get it to work with a customTemplate.
./src/main/resources/custom_template/ApiClient.mustache.detomarco commentedon Dec 8, 2025
Is it possible to extend the request in order to add jackson 3 support to kotlin libray as well?
desperateCoder commentedon Jan 22, 2026
+1
Consider using
useSpringBoot4as a key, since it correlates touseSpringBoot3in theconfigOptionsantechrestos commentedon Jan 29, 2026
I think that
useJackson3anduseSpringBoot4needs to remain separated.useJackson3may be usedas spotted by @detomarco , the impact for adding jackson 3 to templates is a heavy work . As an example, I searched the
import com.fasterxml.jacksonstring in all the mustache files of the project in7.19.0and 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.
rupert-jung-mw commentedon Jan 29, 2026
Maybe just use separate keys for client (RestClient vs. RestTemplate) and JacksonVersion (2 vs. 3)
aitkarra commentedon Jan 30, 2026
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).
feature(jackson3) add jackson3 support for spring generator
ramondebeijer commentedon Jan 30, 2026
@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 usesorg.springframework.web.client.ResponseSpecdon't you have that issue?paul-kraftlauget commentedon Jan 30, 2026
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.
antechrestos commentedon Jan 30, 2026
I am about to push a PR for spring generator
46 remaining items
Merge branch 'master' into feature/OpenAPIToolsGH-22294-rest-template…
jorgerod commentedon Mar 5, 2026
Hi
I created this PR to support Jackson 3 in Java RestTemplate.
Regrouping the PRs
yonatankarp commentedon Mar 6, 2026
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
kotlinclient generator, which I hope(!) will not be as big.muchtar5 commentedon Mar 9, 2026
Please use the same key as they are used for the spring server and also consider implementing useSpringBoot4 and useJackson3 for kotlin client.
yonatankarp commentedon Mar 9, 2026
@muchtar5 I will, just in a seperate pr, it's already a bit out of control with 3.5k lines changed
muchtar5 commentedon Mar 9, 2026
thanks, could you please link the PR here, that would help me and future requests
yonatankarp commentedon Mar 9, 2026
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 😄
stil4m commentedon Mar 10, 2026
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
recordsyntax 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 ;)
feature(jackson3) add jackson3 support for spring generator (OpenAPIT…
feature/jackson3 native (fixes OpenAPITools#22294) (OpenAPITools#23071)
ann94a commentedon Apr 9, 2026
Hello,
Will you support the library feign?
Best regards
torstenwerner commentedon Apr 12, 2026
Why do you comment an issue related to Jackson3? Please read the documentation at https://openapi-generator.tech/docs/generators/java/ . It mentions feign.
[Java] [apache-httpclient] Add support for Jackson3 for apache httpcl…
christian-ertl commentedon Jul 10, 2026
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.