From 70d5135d55a7bdfbcfebea1cc4b7b44a4fe57931 Mon Sep 17 00:00:00 2001 From: Yurii Titov Date: Tue, 3 Mar 2026 15:53:56 +0100 Subject: [PATCH 1/4] fix: duplicated discriminator serialisation issue --- .../src/main/templates/boat-spring/typeInfoAnnotation.mustache | 1 - 1 file changed, 1 deletion(-) diff --git a/boat-scaffold/src/main/templates/boat-spring/typeInfoAnnotation.mustache b/boat-scaffold/src/main/templates/boat-spring/typeInfoAnnotation.mustache index 4f49c4a0f..c2b7f0c89 100644 --- a/boat-scaffold/src/main/templates/boat-spring/typeInfoAnnotation.mustache +++ b/boat-scaffold/src/main/templates/boat-spring/typeInfoAnnotation.mustache @@ -3,7 +3,6 @@ {{#-first}} @JsonIgnoreProperties( value = "{{{discriminator.propertyBaseName}}}", // ignore manually set {{{discriminator.propertyBaseName}}}, it will be automatically generated by Jackson during serialization - allowGetters = true, // allows the {{{discriminator.propertyBaseName}}} to be set during serialization allowSetters = true // allows the {{{discriminator.propertyBaseName}}} to be set during deserialization ) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "{{{discriminator.propertyBaseName}}}", visible = true) From 71d62148eb8894733980be9f66bfb7df72aa7fd6 Mon Sep 17 00:00:00 2001 From: Yurii Titov Date: Wed, 4 Mar 2026 14:38:27 +0100 Subject: [PATCH 2/4] fix: add test cases --- .../java/BoatSpringTemplatesTests.java | 53 ++++++++++++++----- 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/boat-scaffold/src/test/java/com/backbase/oss/codegen/java/BoatSpringTemplatesTests.java b/boat-scaffold/src/test/java/com/backbase/oss/codegen/java/BoatSpringTemplatesTests.java index d43b09220..2efb4e825 100644 --- a/boat-scaffold/src/test/java/com/backbase/oss/codegen/java/BoatSpringTemplatesTests.java +++ b/boat-scaffold/src/test/java/com/backbase/oss/codegen/java/BoatSpringTemplatesTests.java @@ -12,11 +12,13 @@ import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.DynamicContainer.dynamicContainer; import static org.junit.jupiter.api.DynamicTest.dynamicTest; import com.backbase.oss.codegen.java.VerificationRunner.Verification; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.type.ArrayType; import com.fasterxml.jackson.databind.type.TypeBindings; @@ -260,21 +262,32 @@ private void verifyGeneratedClasses(File projectDir) throws Exception { private void verifyReceivableRequestModelJsonConversion(ClassLoader classLoader) throws InterruptedException { String testedModelClassName = buildReceivableRequestModelClassName(); + String parentModelClassName = buildPaymentRequestModelClassName(); var objectMapper = new ObjectMapper(); Runnable verificationRunnable = () -> { try { Class modelClass = classLoader.loadClass(testedModelClassName); + Class parentClass = classLoader.loadClass(parentModelClassName); Constructor constructor = modelClass.getConstructor(String.class, String.class, String.class); Object modelObject1 = constructor.newInstance("OK_status", "ref123", "EUR"); Object modelObject2 = constructor.newInstance("BAD_status", "ref456", "USD"); + // Serialize using the parent (discriminated) type so that Jackson's + // @JsonTypeInfo writes the discriminator property into the JSON. + // With allowGetters=true removed from @JsonIgnoreProperties the getter + // is fully ignored, so the polymorphic type info is the only source of + // the discriminator during serialization. + TypeFactory tf = TypeFactory.defaultInstance(); + // serialize and deserialize list List modelObjects = List.of(modelObject1, modelObject2); - String serializedObjects = objectMapper.writeValueAsString(modelObjects); + String serializedObjects = objectMapper.writerFor( + tf.constructCollectionType(List.class, parentClass) + ).writeValueAsString(modelObjects); Object[] deserializedModelObjects = objectMapper.readValue( serializedObjects, ArrayType.construct( - TypeFactory.defaultInstance().constructFromCanonical(modelClass.getName()), + tf.constructFromCanonical(parentClass.getName()), TypeBindings.emptyBindings() ) ); @@ -282,10 +295,12 @@ private void verifyReceivableRequestModelJsonConversion(ClassLoader classLoader) assertEquals(modelObject1.getClass(), deserializedModelObjects[0].getClass()); // serialize and deserialize single object - String serializedObject1 = objectMapper.writeValueAsString(modelObject1); - Object deserializedObject1 = objectMapper.readValue(serializedObject1, modelClass); + String serializedObject1 = objectMapper.writerFor(parentClass).writeValueAsString(modelObject1); + Object deserializedObject1 = objectMapper.readValue(serializedObject1, parentClass); assertEquals(modelClass, deserializedObject1.getClass()); + verifyJsonIgnoreAnnotation(modelClass); + verifyJsonIgnoreAnnotation(parentClass); } catch (Exception e) { throw new UnhandledException(e); } @@ -296,6 +311,15 @@ private void verifyReceivableRequestModelJsonConversion(ClassLoader classLoader) ); } + private void verifyJsonIgnoreAnnotation(Class modelClass) { + JsonIgnoreProperties ignoreAnnotation = modelClass.getAnnotation(JsonIgnoreProperties.class); + + if (ignoreAnnotation != null) { + assertFalse(ignoreAnnotation.allowGetters(), + "Class " + modelClass.getName() + " should not have allowGetters=true w @JsonIgnoreProperties!"); + } + } + private void verifyMultiLineRequest(ClassLoader classLoader) throws InterruptedException { String testedModelClassName = buildMultiLineRequestModelClassName(); Runnable verificationRunnable = () -> { @@ -319,23 +343,28 @@ private void verifyMultiLineRequest(ClassLoader classLoader) throws InterruptedE * Build proper class name for `ReceivableRequest`. */ private String buildReceivableRequestModelClassName() { + return buildModelClassName("ReceivableRequest"); + } + + /** + * Build proper class name for `PaymentRequest` (parent/discriminator base). + */ + private String buildPaymentRequestModelClassName() { + return buildModelClassName("PaymentRequest"); + } + + private String buildModelClassName(String baseName) { var modelPackage = param.name.replace('-', '.') + ".model"; var classNameSuffix = org.apache.commons.lang3.StringUtils.capitalize( param.name.indexOf('-') > -1 ? CaseFormat.LOWER_HYPHEN.to(CaseFormat.LOWER_CAMEL, param.name) : param.name ); - return modelPackage + ".ReceivableRequest" + classNameSuffix; + return modelPackage + "." + baseName + classNameSuffix; } private String buildMultiLineRequestModelClassName() { - var modelPackage = param.name.replace('-', '.') + ".model"; - var classNameSuffix = org.apache.commons.lang3.StringUtils.capitalize( - param.name.indexOf('-') > -1 - ? CaseFormat.LOWER_HYPHEN.to(CaseFormat.LOWER_CAMEL, param.name) - : param.name - ); - return modelPackage + ".MultiLinePaymentRequest" + classNameSuffix; + return buildModelClassName("MultiLinePaymentRequest"); } private boolean findPattern(String filePattern, String linePattern) { From c7c6d2afbe11906c6f3abf69738cbdad3d5fab40 Mon Sep 17 00:00:00 2001 From: Yurii Titov Date: Wed, 4 Mar 2026 15:42:25 +0100 Subject: [PATCH 3/4] chore: small changes in assert message --- .../oss/codegen/java/BoatSpringTemplatesTests.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/boat-scaffold/src/test/java/com/backbase/oss/codegen/java/BoatSpringTemplatesTests.java b/boat-scaffold/src/test/java/com/backbase/oss/codegen/java/BoatSpringTemplatesTests.java index 2efb4e825..36c00f407 100644 --- a/boat-scaffold/src/test/java/com/backbase/oss/codegen/java/BoatSpringTemplatesTests.java +++ b/boat-scaffold/src/test/java/com/backbase/oss/codegen/java/BoatSpringTemplatesTests.java @@ -273,10 +273,7 @@ private void verifyReceivableRequestModelJsonConversion(ClassLoader classLoader) Object modelObject2 = constructor.newInstance("BAD_status", "ref456", "USD"); // Serialize using the parent (discriminated) type so that Jackson's - // @JsonTypeInfo writes the discriminator property into the JSON. - // With allowGetters=true removed from @JsonIgnoreProperties the getter - // is fully ignored, so the polymorphic type info is the only source of - // the discriminator during serialization. + // is aware of full polymorphic context TypeFactory tf = TypeFactory.defaultInstance(); // serialize and deserialize list @@ -316,7 +313,7 @@ private void verifyJsonIgnoreAnnotation(Class modelClass) { if (ignoreAnnotation != null) { assertFalse(ignoreAnnotation.allowGetters(), - "Class " + modelClass.getName() + " should not have allowGetters=true w @JsonIgnoreProperties!"); + "Class " + modelClass.getName() + " should not have allowGetters=true in @JsonIgnoreProperties"); } } From 2da0757731f7b7acae607f329aec1546e92de1b8 Mon Sep 17 00:00:00 2001 From: Yurii Titov Date: Wed, 4 Mar 2026 16:30:17 +0100 Subject: [PATCH 4/4] chore: add release note --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 0cd3191a9..eebed2a8a 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,10 @@ It currently consists of # Release Notes BOAT is still under development and subject to change. + +## 0.17.75 +* Fixed duplicate serialization of the discriminator property in Jackson-based Java models by removing allowGetters = true from the @JsonIgnoreProperties annotation. + ## 0.17.74 * Swift5: Removed deprecated initializer from model objects. The initializer is now internal and only accessible through the Builder pattern, preventing breaking code from deprecation warnings.