diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bd513eb618..bcacb44cf63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ * Fix #6941: HasMetadata.getApiVersion no slash when empty group * Fix #6982: (java-generator) Double default field values with `d` suffix * Fix #6987: Kube API Test startup fails on readiness SSL check +* Fix #7036: Resolve serialization errors after Jackson 2.19.0 upgrade (breaks older versions) * Fix #7037: getKubernetesVersion works in Kubernetes v1.33.0 #### Improvements diff --git a/kubernetes-model-generator/kubernetes-model-common/src/main/java/io/fabric8/kubernetes/model/jackson/SettableBeanPropertyDelegating.java b/kubernetes-model-generator/kubernetes-model-common/src/main/java/io/fabric8/kubernetes/model/jackson/SettableBeanPropertyDelegating.java index f2cb114f87f..4cd400b7a0e 100644 --- a/kubernetes-model-generator/kubernetes-model-common/src/main/java/io/fabric8/kubernetes/model/jackson/SettableBeanPropertyDelegating.java +++ b/kubernetes-model-generator/kubernetes-model-common/src/main/java/io/fabric8/kubernetes/model/jackson/SettableBeanPropertyDelegating.java @@ -27,6 +27,7 @@ import com.fasterxml.jackson.databind.exc.MismatchedInputException; import com.fasterxml.jackson.databind.introspect.ObjectIdInfo; import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonObjectFormatVisitor; +import com.fasterxml.jackson.databind.util.NameTransformer; import java.io.IOException; import java.lang.annotation.Annotation; @@ -147,6 +148,14 @@ public SettableBeanProperty withSimpleName(String simpleName) { return _with(delegate.withSimpleName(simpleName)); } + /** + * {@inheritDoc} + */ + @Override + public SettableBeanProperty unwrapped(NameTransformer unwrapper) { + return _with(delegate.unwrapped(unwrapper)); + } + /** * {@inheritDoc} */ diff --git a/kubernetes-model-generator/kubernetes-model-common/src/main/java/io/fabric8/kubernetes/model/jackson/UnmatchedFieldTypeModule.java b/kubernetes-model-generator/kubernetes-model-common/src/main/java/io/fabric8/kubernetes/model/jackson/UnmatchedFieldTypeModule.java index 4b8dc5a94ac..589a7ae8546 100644 --- a/kubernetes-model-generator/kubernetes-model-common/src/main/java/io/fabric8/kubernetes/model/jackson/UnmatchedFieldTypeModule.java +++ b/kubernetes-model-generator/kubernetes-model-common/src/main/java/io/fabric8/kubernetes/model/jackson/UnmatchedFieldTypeModule.java @@ -20,10 +20,14 @@ import com.fasterxml.jackson.databind.SerializationConfig; import com.fasterxml.jackson.databind.deser.BeanDeserializerBuilder; import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier; +import com.fasterxml.jackson.databind.introspect.AnnotatedMember; import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.BeanPropertyWriter; import com.fasterxml.jackson.databind.ser.BeanSerializerBuilder; import com.fasterxml.jackson.databind.ser.BeanSerializerModifier; +import java.lang.reflect.Member; +import java.util.List; import java.util.stream.Collectors; /** @@ -61,13 +65,40 @@ public BeanDeserializerBuilder updateBuilder(DeserializationConfig config, BeanD } }); setSerializerModifier(new BeanSerializerModifier() { + /** + * Customizes property writers used during serialization. + * + * This method wraps standard property writers in a delegate that can suppress serialization + * of fields that may be overridden by values in the map returned by {@code @JsonAnyGetter}. + * + * The property corresponding to the {@code @JsonAnyGetter} method itself is left unmodified + * to avoid interfering with Jackson’s internal handling via {@link com.fasterxml.jackson.databind.ser.AnyGetterWriter}. + * + * This mechanism ensures that if a field is both explicitly declared and also present in the + * any-getter map, only the value from the any-getter is serialized, avoiding duplication or conflicts. + */ @Override public BeanSerializerBuilder updateBuilder(SerializationConfig config, BeanDescription beanDesc, BeanSerializerBuilder builder) { - builder.setProperties(builder.getProperties().stream() - .map(p -> new BeanPropertyWriterDelegate(p, builder.getBeanDescription().findAnyGetter(), - UnmatchedFieldTypeModule.this::isLogWarnings)) - .collect(Collectors.toList())); + AnnotatedMember anyGetter = beanDesc.findAnyGetter(); + Member anyGetterMember = (anyGetter != null) ? anyGetter.getMember() : null; + + // Wrap each property writer unless it's the any-getter (handled by Jackson separately) + List customWriters = builder.getProperties().stream() + .map(writer -> { + if (isAnyGetterWriter(writer, anyGetterMember)) { + // Skipping wrapping for any-getter to avoid interfering with Jackson's internal handling + return writer; + } + // Wrap normal field writers with delegate to handle overrides and logging + return new BeanPropertyWriterDelegate( + writer, + anyGetter, + UnmatchedFieldTypeModule.this::isLogWarnings); + }) + .collect(Collectors.toList()); + + builder.setProperties(customWriters); return builder; } }); @@ -115,4 +146,22 @@ public static void removeInTemplate() { IN_TEMPLATE.remove(); } + /** + * Checks whether the given {@link BeanPropertyWriter} corresponds to the method annotated with {@code @JsonAnyGetter}. + * + * This is used to identify and exclude the any-getter property from custom wrapping, + * since Jackson handles it separately via {@link com.fasterxml.jackson.databind.ser.AnyGetterWriter}. + * + * @param writer the property writer to examine + * @param anyGetterMember the reflective member (method or field) marked with {@code @JsonAnyGetter}, if available + * @return {@code true} if the writer represents the any-getter property; {@code false} otherwise + */ + private boolean isAnyGetterWriter(BeanPropertyWriter writer, Member anyGetterMember) { + if (writer == null || anyGetterMember == null) { + return false; + } + AnnotatedMember annotated = writer.getMember(); + Member member = (annotated != null) ? annotated.getMember() : null; + return member != null && member.equals(anyGetterMember); + } } diff --git a/kubernetes-model-generator/kubernetes-model-common/src/test/java/io/fabric8/kubernetes/model/jackson/SettableBeanPropertyDelegatingTest.java b/kubernetes-model-generator/kubernetes-model-common/src/test/java/io/fabric8/kubernetes/model/jackson/SettableBeanPropertyDelegatingTest.java index 21072a33df1..80dac4ad0ec 100644 --- a/kubernetes-model-generator/kubernetes-model-common/src/test/java/io/fabric8/kubernetes/model/jackson/SettableBeanPropertyDelegatingTest.java +++ b/kubernetes-model-generator/kubernetes-model-common/src/test/java/io/fabric8/kubernetes/model/jackson/SettableBeanPropertyDelegatingTest.java @@ -57,8 +57,10 @@ import java.lang.reflect.AccessibleObject; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; @@ -475,7 +477,7 @@ void deserializeSetAndReturnWithExceptionAndNullAnySetter() throws IOException { class ReflectionTest { @Test - @DisplayName("all methods from superclass (SettableBeanProperty) are implemented by delegating class (SettableBeanPropertyDelegate)") + @DisplayName("All concrete superclass methods are implemented by SettableBeanPropertyDelegating") void allMethodsFromSuperclassAreImplementedByDelegatingClass() { final Map superclassMethods = Stream.of(SettableBeanProperty.class.getDeclaredMethods()) .filter(m -> !Modifier.isFinal(m.getModifiers())) @@ -484,15 +486,23 @@ void allMethodsFromSuperclassAreImplementedByDelegatingClass() { .filter(m -> !m.getName().startsWith("_")) .map(MethodSignature::from) .collect(Collectors.toMap(ms -> ms, ms -> false)); + Stream.concat( Stream.of(SettableBeanProperty.Delegating.class.getDeclaredMethods()), Stream.of(SettableBeanPropertyDelegating.class.getDeclaredMethods())) .map(MethodSignature::from) .forEach(ms -> superclassMethods.computeIfPresent(ms, (k, v) -> true)); - assertThat(superclassMethods) - .values() - .containsOnly(true); + + List missing = superclassMethods.entrySet().stream() + .filter(e -> !e.getValue()) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + + assertThat(missing) + .withFailMessage("Missing method overrides: %s", missing) + .isEmpty(); } + } @JsonIgnoreProperties(ignoreUnknown = true) @@ -530,5 +540,12 @@ private static MethodSignature from(Method m) { return new MethodSignature(m.getReturnType(), m.getName(), m.getParameterTypes()); } + @Override + public String toString() { + String params = Arrays.stream(parameterTypes) + .map(Class::getSimpleName) + .collect(Collectors.joining(", ")); + return returnType.getSimpleName() + " " + name + "(" + params + ")"; + } } } diff --git a/pom.xml b/pom.xml index cb5430a6b23..3808477bc23 100644 --- a/pom.xml +++ b/pom.xml @@ -92,7 +92,7 @@ 0.200.3 4.12.0 - 2.18.3 + 2.19.0 ${jackson.version} 11.0.25 3.9.9