Skip to content

fix #7036: Jackson 2.19.0 update issues #7038

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -147,6 +148,14 @@ public SettableBeanProperty withSimpleName(String simpleName) {
return _with(delegate.withSimpleName(simpleName));
}

/**
* {@inheritDoc}
*/
@Override
Copy link
Author

@r1c4r60 r1c4r60 Apr 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: This @Override is not backward compatible with Jackson versions prior to 2.19.0.

I do have ready local changes using Reflection and package version checks to restore backward compatibility if needed.

public SettableBeanProperty unwrapped(NameTransformer unwrapper) {
return _with(delegate.unwrapped(unwrapper));
}

/**
* {@inheritDoc}
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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<BeanPropertyWriter> 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;
}
});
Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -475,7 +477,7 @@ void deserializeSetAndReturnWithExceptionAndNullAnySetter() throws IOException {
class ReflectionTest {

@Test
@DisplayName("all methods from superclass (SettableBeanProperty) are implemented by delegating class (SettableBeanPropertyDelegate)")
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SettableBeanPropertyDelegate does not exist.

@DisplayName("All concrete superclass methods are implemented by SettableBeanPropertyDelegating")
void allMethodsFromSuperclassAreImplementedByDelegatingClass() {
final Map<MethodSignature, Boolean> superclassMethods = Stream.of(SettableBeanProperty.class.getDeclaredMethods())
.filter(m -> !Modifier.isFinal(m.getModifiers()))
Expand All @@ -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<MethodSignature> 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)
Expand Down Expand Up @@ -530,5 +540,12 @@ private static MethodSignature from(Method m) {
return new MethodSignature(m.getReturnType(), m.getName(), m.getParameterTypes());
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Improves how we report findings.

Now:

Screenshot 2025-04-25 at 21 05 42

Note: withSimpleName was just a local test. It wasn't missing.

Previously:

Screenshot 2025-04-25 at 20 57 13

@Override
public String toString() {
String params = Arrays.stream(parameterTypes)
.map(Class::getSimpleName)
.collect(Collectors.joining(", "));
return returnType.getSimpleName() + " " + name + "(" + params + ")";
}
}
}
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
<!-- Core versions -->
<sundrio.version>0.200.3</sundrio.version>
<okhttp.version>4.12.0</okhttp.version>
<jackson.version>2.18.3</jackson.version>
<jackson.version>2.19.0</jackson.version>
<jackson.bundle.version>${jackson.version}</jackson.bundle.version>
<jetty.version>11.0.25</jetty.version>
<maven-core.version>3.9.9</maven-core.version>
Expand Down