From 2d155105a41c5f409115e0d783dbc7c833cc8c83 Mon Sep 17 00:00:00 2001 From: gf-smtzgr <143621187+gf-smtzgr@users.noreply.github.com> Date: Mon, 4 Mar 2024 05:50:42 +0100 Subject: [PATCH] Added jackson as supported serialization library for retrofit2 (#16853) * added jackson as supported serialization library for retrofit2; resolves #7435 * Jackson support for retrofit2 library: Adjusted ApiClient to respectively include only gson or jackson specific conversion support depending on which serialization framework is selected * Jackson support for retrofit2 library: Adjusted dependencies to respectively only include those necessary for gson or jackson depending on which serialization framework is selected * reorder converter factory additions to have minimal change * -: skipping gson-fire dependency when gson is not selected * -: Jackson support for retrofit2 library: since useplayws implies jackson usage, only adding dependencies via useplayws that are specific to it * Jackson support for retrofit2 library: fixed whitespace issue in generated api clients * Jackson support for retrofit2 library: removed duplicated play26 dependency version property * Jackson support for retrofit2 library: update to play26 example as that had gson dependencies but does not seem to use it and now fully relies on the jackson setting to control the respective dependencies; play version property and dependency changed place because the jackson dependencies are grouped together and the play26 ones are placed after * Jackson support for retrofit2 library: adjusting dependencies also for gradle file * -: moved jackson databind version out of jackson section since it is used independently of jackson support it seems * -: update gradle dependencies to match changes in maven dependencies --- .../codegen/languages/JavaClientCodegen.java | 5 +- .../libraries/retrofit2/ApiClient.mustache | 18 +- .../libraries/retrofit2/JSON_jackson.mustache | 261 ++++++++++++++++++ .../libraries/retrofit2/build.gradle.mustache | 14 +- .../Java/libraries/retrofit2/pom.mustache | 34 ++- .../java/retrofit2-play26/build.gradle | 6 +- .../petstore/java/retrofit2-play26/pom.xml | 29 +- .../petstore/java/retrofit2/build.gradle | 2 + .../petstore/java/retrofit2rx2/build.gradle | 2 + .../petstore/java/retrofit2rx3/build.gradle | 2 + 10 files changed, 339 insertions(+), 34 deletions(-) create mode 100644 modules/openapi-generator/src/main/resources/Java/libraries/retrofit2/JSON_jackson.mustache diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java index 6c390a9b47..f16fbf15a1 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java @@ -606,8 +606,9 @@ public class JavaClientCodegen extends AbstractJavaCodegen } else if (RETROFIT_2.equals(getLibrary())) { supportingFiles.add(new SupportingFile("auth/OAuthOkHttpClient.mustache", authFolder, "OAuthOkHttpClient.java")); supportingFiles.add(new SupportingFile("CollectionFormats.mustache", invokerFolder, "CollectionFormats.java")); - forceSerializationLibrary(SERIALIZATION_LIBRARY_GSON); - if (RETROFIT_2.equals(getLibrary()) && !usePlayWS) { + if (SERIALIZATION_LIBRARY_JACKSON.equals(getSerializationLibrary())) { + supportingFiles.add(new SupportingFile("JSON_jackson.mustache", invokerFolder, "JSON.java")); + } else if (!usePlayWS) { supportingFiles.add(new SupportingFile("JSON.mustache", invokerFolder, "JSON.java")); } } else if (JERSEY2.equals(getLibrary())) { diff --git a/modules/openapi-generator/src/main/resources/Java/libraries/retrofit2/ApiClient.mustache b/modules/openapi-generator/src/main/resources/Java/libraries/retrofit2/ApiClient.mustache index d5abd018cb..a4abf07f45 100644 --- a/modules/openapi-generator/src/main/resources/Java/libraries/retrofit2/ApiClient.mustache +++ b/modules/openapi-generator/src/main/resources/Java/libraries/retrofit2/ApiClient.mustache @@ -1,8 +1,10 @@ package {{invokerPackage}}; +{{#gson}} import com.google.gson.Gson; import com.google.gson.JsonParseException; import com.google.gson.JsonElement; +{{/gson}} import okhttp3.Interceptor; import okhttp3.OkHttpClient; import okhttp3.RequestBody; @@ -22,7 +24,12 @@ import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; {{#useRxJava3}} import hu.akarnokd.rxjava3.retrofit.RxJava3CallAdapterFactory; {{/useRxJava3}} +{{#gson}} import retrofit2.converter.gson.GsonConverterFactory; +{{/gson}} +{{#jackson}} +import retrofit2.converter.jackson.JacksonConverterFactory; +{{/jackson}} import retrofit2.converter.scalars.ScalarsConverterFactory; import {{invokerPackage}}.auth.HttpBasicAuth; import {{invokerPackage}}.auth.HttpBearerAuth; @@ -157,7 +164,12 @@ public class ApiClient { .addCallAdapterFactory(RxJava3CallAdapterFactory.create()) {{/useRxJava3}} .addConverterFactory(ScalarsConverterFactory.create()) + {{#jackson}} + .addConverterFactory(JacksonConverterFactory.create(json.getMapper())); + {{/jackson}} + {{#gson}} .addConverterFactory(GsonCustomConverterFactory.create(json.getGson())); + {{/gson}} } public S createService(Class serviceClass) { @@ -173,6 +185,7 @@ public class ApiClient { return this; } + {{#gson}} public ApiClient setSqlDateFormat(DateFormat dateFormat) { this.json.setSqlDateFormat(dateFormat); return this; @@ -200,8 +213,9 @@ public class ApiClient { this.json.setLocalDateFormat(dateFormat); return this; } - {{/jsr310}} + {{/gson}} + /** * Helper method to configure the first api key found @@ -401,6 +415,7 @@ public class ApiClient { } } +{{#gson}} /** * This wrapper is to take care of this case: * when the deserialization fails due to JsonParseException and the @@ -455,3 +470,4 @@ class GsonCustomConverterFactory extends Converter.Factory return gsonConverterFactory.requestBodyConverter(type, parameterAnnotations, methodAnnotations, retrofit); } } +{{/gson}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/Java/libraries/retrofit2/JSON_jackson.mustache b/modules/openapi-generator/src/main/resources/Java/libraries/retrofit2/JSON_jackson.mustache new file mode 100644 index 0000000000..25287dcf7f --- /dev/null +++ b/modules/openapi-generator/src/main/resources/Java/libraries/retrofit2/JSON_jackson.mustache @@ -0,0 +1,261 @@ +package {{invokerPackage}}; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.json.JsonMapper; +{{#openApiNullable}} +import org.openapitools.jackson.nullable.JsonNullableModule; +{{/openApiNullable}} +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +{{#joda}} +import com.fasterxml.jackson.datatype.joda.JodaModule; +{{/joda}} +{{#models.0}} +import {{modelPackage}}.*; +{{/models.0}} + +import java.text.DateFormat; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import {{javaxPackage}}.ws.rs.core.GenericType; +import {{javaxPackage}}.ws.rs.ext.ContextResolver; + +{{>generatedAnnotation}} +public class JSON implements ContextResolver { + private ObjectMapper mapper; + + public JSON() { + mapper = JsonMapper.builder() + .serializationInclusion(JsonInclude.Include.NON_NULL) + .configure(MapperFeature.ALLOW_COERCION_OF_SCALARS, false) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true) + .configure(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE, true) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING) + .enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING) + .defaultDateFormat(new RFC3339DateFormat()) + .addModule(new JavaTimeModule()) + {{#joda}} + .addModule(new JodaModule()) + {{/joda}} + {{#openApiNullable}} + .addModule(new JsonNullableModule()) + {{/openApiNullable}} + .build(); + } + + /** + * Set the date format for JSON (de)serialization with Date properties. + * @param dateFormat Date format + */ + public void setDateFormat(DateFormat dateFormat) { + mapper.setDateFormat(dateFormat); + } + + @Override + public ObjectMapper getContext(Class type) { + return mapper; + } + + /** + * Get the object mapper + * + * @return object mapper + */ + public ObjectMapper getMapper() { return mapper; } + + /** + * Returns the target model class that should be used to deserialize the input data. + * The discriminator mappings are used to determine the target model class. + * + * @param node The input data. + * @param modelClass The class that contains the discriminator mappings. + */ + public static Class getClassForElement(JsonNode node, Class modelClass) { + ClassDiscriminatorMapping cdm = modelDiscriminators.get(modelClass); + if (cdm != null) { + return cdm.getClassForElement(node, new HashSet<>()); + } + return null; + } + + /** + * Helper class to register the discriminator mappings. + */ + private static class ClassDiscriminatorMapping { + // The model class name. + Class modelClass; + // The name of the discriminator property. + String discriminatorName; + // The discriminator mappings for a model class. + Map> discriminatorMappings; + + // Constructs a new class discriminator. + ClassDiscriminatorMapping(Class cls, String propertyName, Map> mappings) { + modelClass = cls; + discriminatorName = propertyName; + discriminatorMappings = new HashMap<>(); + if (mappings != null) { + discriminatorMappings.putAll(mappings); + } + } + + // Return the name of the discriminator property for this model class. + String getDiscriminatorPropertyName() { + return discriminatorName; + } + + // Return the discriminator value or null if the discriminator is not + // present in the payload. + String getDiscriminatorValue(JsonNode node) { + // Determine the value of the discriminator property in the input data. + if (discriminatorName != null) { + // Get the value of the discriminator property, if present in the input payload. + node = node.get(discriminatorName); + if (node != null && node.isValueNode()) { + String discrValue = node.asText(); + if (discrValue != null) { + return discrValue; + } + } + } + return null; + } + + /** + * Returns the target model class that should be used to deserialize the input data. + * This function can be invoked for anyOf/oneOf composed models with discriminator mappings. + * The discriminator mappings are used to determine the target model class. + * + * @param node The input data. + * @param visitedClasses The set of classes that have already been visited. + */ + Class getClassForElement(JsonNode node, Set> visitedClasses) { + if (visitedClasses.contains(modelClass)) { + // Class has already been visited. + return null; + } + // Determine the value of the discriminator property in the input data. + String discrValue = getDiscriminatorValue(node); + if (discrValue == null) { + return null; + } + Class cls = discriminatorMappings.get(discrValue); + // It may not be sufficient to return this cls directly because that target class + // may itself be a composed schema, possibly with its own discriminator. + visitedClasses.add(modelClass); + for (Class childClass : discriminatorMappings.values()) { + ClassDiscriminatorMapping childCdm = modelDiscriminators.get(childClass); + if (childCdm == null) { + continue; + } + if (!discriminatorName.equals(childCdm.discriminatorName)) { + discrValue = getDiscriminatorValue(node); + if (discrValue == null) { + continue; + } + } + if (childCdm != null) { + // Recursively traverse the discriminator mappings. + Class childDiscr = childCdm.getClassForElement(node, visitedClasses); + if (childDiscr != null) { + return childDiscr; + } + } + } + return cls; + } + } + + /** + * Returns true if inst is an instance of modelClass in the OpenAPI model hierarchy. + * + * The Java class hierarchy is not implemented the same way as the OpenAPI model hierarchy, + * so it's not possible to use the instanceof keyword. + * + * @param modelClass A OpenAPI model class. + * @param inst The instance object. + */ + public static boolean isInstanceOf(Class modelClass, Object inst, Set> visitedClasses) { + if (modelClass.isInstance(inst)) { + // This handles the 'allOf' use case with single parent inheritance. + return true; + } + if (visitedClasses.contains(modelClass)) { + // This is to prevent infinite recursion when the composed schemas have + // a circular dependency. + return false; + } + visitedClasses.add(modelClass); + + // Traverse the oneOf/anyOf composed schemas. + Map descendants = modelDescendants.get(modelClass); + if (descendants != null) { + for (GenericType childType : descendants.values()) { + if (isInstanceOf(childType.getRawType(), inst, visitedClasses)) { + return true; + } + } + } + return false; + } + + /** + * A map of discriminators for all model classes. + */ + private static Map, ClassDiscriminatorMapping> modelDiscriminators = new HashMap<>(); + + /** + * A map of oneOf/anyOf descendants for each model class. + */ + private static Map, Map> modelDescendants = new HashMap<>(); + + /** + * Register a model class discriminator. + * + * @param modelClass the model class + * @param discriminatorPropertyName the name of the discriminator property + * @param mappings a map with the discriminator mappings. + */ + public static void registerDiscriminator(Class modelClass, String discriminatorPropertyName, Map> mappings) { + ClassDiscriminatorMapping m = new ClassDiscriminatorMapping(modelClass, discriminatorPropertyName, mappings); + modelDiscriminators.put(modelClass, m); + } + + /** + * Register the oneOf/anyOf descendants of the modelClass. + * + * @param modelClass the model class + * @param descendants a map of oneOf/anyOf descendants. + */ + public static void registerDescendants(Class modelClass, Map descendants) { + modelDescendants.put(modelClass, descendants); + } + + private static JSON json; + + static + { + json = new JSON(); + } + + /** + * Get the default JSON instance. + * + * @return the default JSON instance + */ + public static JSON getDefault() { + return json; + } + + /** + * Set the default JSON instance. + * + * @param json JSON instance to be used + */ + public static void setDefault(JSON json) { + JSON.json = json; + } +} diff --git a/modules/openapi-generator/src/main/resources/Java/libraries/retrofit2/build.gradle.mustache b/modules/openapi-generator/src/main/resources/Java/libraries/retrofit2/build.gradle.mustache index a21ced55be..522c475f1c 100644 --- a/modules/openapi-generator/src/main/resources/Java/libraries/retrofit2/build.gradle.mustache +++ b/modules/openapi-generator/src/main/resources/Java/libraries/retrofit2/build.gradle.mustache @@ -99,12 +99,15 @@ if(hasProperty('target') && target == 'android') { ext { oltu_version = "1.0.1" retrofit_version = "2.3.0" - {{#usePlayWS}} - jackson_version = "2.15.2" jackson_databind_version = "2.15.2" + {{#jackson}} + jackson_version = "2.15.2" + javax_ws_rs_api_version = "2.1.1" {{#openApiNullable}} jackson_databind_nullable_version = "0.2.6" {{/openApiNullable}} + {{/jackson}} + {{#usePlayWS}} play_version = "2.6.7" {{/usePlayWS}} jakarta_annotation_version = "1.3.5" @@ -145,16 +148,19 @@ dependencies { {{/joda}} {{#usePlayWS}} implementation "com.typesafe.play:play-ahc-ws_2.12:$play_version" + {{/usePlayWS}} + {{#jackson}} implementation "jakarta.validation:jakarta.validation-api:2.0.2" implementation "com.squareup.retrofit2:converter-jackson:$retrofit_version" implementation "com.fasterxml.jackson.core:jackson-core:$jackson_version" implementation "com.fasterxml.jackson.core:jackson-annotations:$jackson_version" - implementation "com.fasterxml.jackson.core:jackson-databind:$jackson_databind_version" + implementation "javax.ws.rs:javax.ws.rs-api:$javax_ws_rs_api_version" {{#openApiNullable}} implementation "org.openapitools:jackson-databind-nullable:$jackson_databind_nullable_version" {{/openApiNullable}} implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jackson_version" - {{/usePlayWS}} + {{/jackson}} + implementation "com.fasterxml.jackson.core:jackson-databind:$jackson_databind_version" implementation "jakarta.annotation:jakarta.annotation-api:$jakarta_annotation_version" testImplementation "junit:junit:$junit_version" } diff --git a/modules/openapi-generator/src/main/resources/Java/libraries/retrofit2/pom.mustache b/modules/openapi-generator/src/main/resources/Java/libraries/retrofit2/pom.mustache index 2bf85c7d24..00edcf10dc 100644 --- a/modules/openapi-generator/src/main/resources/Java/libraries/retrofit2/pom.mustache +++ b/modules/openapi-generator/src/main/resources/Java/libraries/retrofit2/pom.mustache @@ -222,11 +222,13 @@ jsr305 3.0.2 + {{#gson}} com.squareup.retrofit2 converter-gson ${retrofit-version} + {{/gson}} com.squareup.retrofit2 retrofit @@ -248,11 +250,13 @@ + {{#gson}} io.gsonfire gson-fire ${gson-fire-version} + {{/gson}} {{#joda}} joda-time @@ -284,7 +288,7 @@ 3.0.0 {{/useRxJava3}} - {{#usePlayWS}} + {{#jackson}} com.squareup.retrofit2 @@ -321,16 +325,25 @@ ${jackson-version} {{/withXml}} - - com.typesafe.play - play-ahc-ws_2.12 - ${play-version} - + {{#useBeanValidation}} jakarta.validation jakarta.validation-api ${beanvalidation-version} + {{/useBeanValidation}} + + javax.ws.rs + javax.ws.rs-api + ${javax.ws.rs-api-version} + + {{/jackson}} + {{#usePlayWS}} + + com.typesafe.play + play-ahc-ws_2.12 + ${play-version} + {{/usePlayWS}} {{#parcelableModel}} @@ -360,15 +373,20 @@ 1.8 ${java.version} ${java.version} + {{#gson}} 1.9.0 + {{/gson}} 1.6.3 2.15.2 - {{#usePlayWS}} + {{#jackson}} 2.15.2 - 2.6.7 {{#openApiNullable}} 0.2.6 {{/openApiNullable}} + 2.1.1 + {{/jackson}} + {{#usePlayWS}} + 2.6.7 {{/usePlayWS}} 2.5.0 {{#useRxJava2}} diff --git a/samples/client/petstore/java/retrofit2-play26/build.gradle b/samples/client/petstore/java/retrofit2-play26/build.gradle index 51ea8801fb..c616155372 100644 --- a/samples/client/petstore/java/retrofit2-play26/build.gradle +++ b/samples/client/petstore/java/retrofit2-play26/build.gradle @@ -99,8 +99,9 @@ if(hasProperty('target') && target == 'android') { ext { oltu_version = "1.0.1" retrofit_version = "2.3.0" - jackson_version = "2.15.2" jackson_databind_version = "2.15.2" + jackson_version = "2.15.2" + javax_ws_rs_api_version = "2.1.1" jackson_databind_nullable_version = "0.2.6" play_version = "2.6.7" jakarta_annotation_version = "1.3.5" @@ -124,9 +125,10 @@ dependencies { implementation "com.squareup.retrofit2:converter-jackson:$retrofit_version" implementation "com.fasterxml.jackson.core:jackson-core:$jackson_version" implementation "com.fasterxml.jackson.core:jackson-annotations:$jackson_version" - implementation "com.fasterxml.jackson.core:jackson-databind:$jackson_databind_version" + implementation "javax.ws.rs:javax.ws.rs-api:$javax_ws_rs_api_version" implementation "org.openapitools:jackson-databind-nullable:$jackson_databind_nullable_version" implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jackson_version" + implementation "com.fasterxml.jackson.core:jackson-databind:$jackson_databind_version" implementation "jakarta.annotation:jakarta.annotation-api:$jakarta_annotation_version" testImplementation "junit:junit:$junit_version" } diff --git a/samples/client/petstore/java/retrofit2-play26/pom.xml b/samples/client/petstore/java/retrofit2-play26/pom.xml index cad74ede3a..fe15d398ed 100644 --- a/samples/client/petstore/java/retrofit2-play26/pom.xml +++ b/samples/client/petstore/java/retrofit2-play26/pom.xml @@ -215,11 +215,6 @@ jsr305 3.0.2 - - com.squareup.retrofit2 - converter-gson - ${retrofit-version} - com.squareup.retrofit2 retrofit @@ -240,11 +235,6 @@ common - - - io.gsonfire - gson-fire - ${gson-fire-version} @@ -272,16 +262,21 @@ jackson-datatype-jsr310 ${jackson-version} - - com.typesafe.play - play-ahc-ws_2.12 - ${play-version} - jakarta.validation jakarta.validation-api ${beanvalidation-version} + + javax.ws.rs + javax.ws.rs-api + ${javax.ws.rs-api-version} + + + com.typesafe.play + play-ahc-ws_2.12 + ${play-version} + jakarta.annotation jakarta.annotation-api @@ -301,12 +296,12 @@ 1.8 ${java.version} ${java.version} - 1.9.0 1.6.3 2.15.2 2.15.2 - 2.6.7 0.2.6 + 2.1.1 + 2.6.7 2.5.0 1.3.5 2.0.2 diff --git a/samples/client/petstore/java/retrofit2/build.gradle b/samples/client/petstore/java/retrofit2/build.gradle index 1a891a2ef8..af7698cba3 100644 --- a/samples/client/petstore/java/retrofit2/build.gradle +++ b/samples/client/petstore/java/retrofit2/build.gradle @@ -99,6 +99,7 @@ if(hasProperty('target') && target == 'android') { ext { oltu_version = "1.0.1" retrofit_version = "2.3.0" + jackson_databind_version = "2.15.2" jakarta_annotation_version = "1.3.5" swagger_annotations_version = "1.5.22" junit_version = "4.13.2" @@ -115,6 +116,7 @@ dependencies { exclude group:'org.apache.oltu.oauth2' , module: 'org.apache.oltu.oauth2.common' } implementation "io.gsonfire:gson-fire:$json_fire_version" + implementation "com.fasterxml.jackson.core:jackson-databind:$jackson_databind_version" implementation "jakarta.annotation:jakarta.annotation-api:$jakarta_annotation_version" testImplementation "junit:junit:$junit_version" } diff --git a/samples/client/petstore/java/retrofit2rx2/build.gradle b/samples/client/petstore/java/retrofit2rx2/build.gradle index 4bad500d24..98ca76ac50 100644 --- a/samples/client/petstore/java/retrofit2rx2/build.gradle +++ b/samples/client/petstore/java/retrofit2rx2/build.gradle @@ -99,6 +99,7 @@ if(hasProperty('target') && target == 'android') { ext { oltu_version = "1.0.1" retrofit_version = "2.3.0" + jackson_databind_version = "2.15.2" jakarta_annotation_version = "1.3.5" swagger_annotations_version = "1.5.22" junit_version = "4.13.2" @@ -118,6 +119,7 @@ dependencies { exclude group:'org.apache.oltu.oauth2' , module: 'org.apache.oltu.oauth2.common' } implementation "io.gsonfire:gson-fire:$json_fire_version" + implementation "com.fasterxml.jackson.core:jackson-databind:$jackson_databind_version" implementation "jakarta.annotation:jakarta.annotation-api:$jakarta_annotation_version" testImplementation "junit:junit:$junit_version" } diff --git a/samples/client/petstore/java/retrofit2rx3/build.gradle b/samples/client/petstore/java/retrofit2rx3/build.gradle index 29bfb6738e..e9a08c4979 100644 --- a/samples/client/petstore/java/retrofit2rx3/build.gradle +++ b/samples/client/petstore/java/retrofit2rx3/build.gradle @@ -99,6 +99,7 @@ if(hasProperty('target') && target == 'android') { ext { oltu_version = "1.0.1" retrofit_version = "2.3.0" + jackson_databind_version = "2.15.2" jakarta_annotation_version = "1.3.5" swagger_annotations_version = "1.5.22" junit_version = "4.13.2" @@ -118,6 +119,7 @@ dependencies { exclude group:'org.apache.oltu.oauth2' , module: 'org.apache.oltu.oauth2.common' } implementation "io.gsonfire:gson-fire:$json_fire_version" + implementation "com.fasterxml.jackson.core:jackson-databind:$jackson_databind_version" implementation "jakarta.annotation:jakarta.annotation-api:$jakarta_annotation_version" testImplementation "junit:junit:$junit_version" }