From 577fe9c74cb41e123e0b8ed1213c8fc50608f262 Mon Sep 17 00:00:00 2001 From: Peter Levart Date: Thu, 13 Jun 2024 17:43:42 +0200 Subject: [PATCH] StdDelegatingSerializer does not consider a Converter that may return null for a non-null input (#4576) --- release-notes/CREDITS-2.x | 5 + release-notes/VERSION-2.x | 3 + .../ser/std/StdDelegatingSerializer.java | 5 + .../databind/ser/TestCustomSerializers.java | 94 +++++++++++++++++-- 4 files changed, 98 insertions(+), 9 deletions(-) diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x index 08c136aa68..a6beb70be5 100644 --- a/release-notes/CREDITS-2.x +++ b/release-notes/CREDITS-2.x @@ -1774,3 +1774,8 @@ Oddbjørn Kvalsund (oddbjornkvalsund@github) * Reported, contributed fix for #4430: Use `ReentrantLock` instead of `synchronized` in `DeserializerCache` to avoid deadlock on pinning (2.17.1) + +Peter Levart (plevart@github) + * Reported, contributed fix for #4575: StdDelegatingSerializer does not consider + a Converter that may return null for a non-null input + (2.17.2) diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index c02c26777a..2781e8b000 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -12,6 +12,9 @@ Project: jackson-databind #4561: Issues using jackson-databind 2.17.1 with Reactor (reported by @wdallastella) +#4575: StdDelegatingSerializer does not consider a Converter that may + return null for a non-null input + (reported, fix contributed by Peter L) 2.17.1 (04-May-2024) diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/std/StdDelegatingSerializer.java b/src/main/java/com/fasterxml/jackson/databind/ser/std/StdDelegatingSerializer.java index 58b6debc1f..6f3b014d22 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/std/StdDelegatingSerializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/std/StdDelegatingSerializer.java @@ -174,6 +174,11 @@ public void serializeWithType(Object value, JsonGenerator gen, SerializerProvide // 03-Oct-2012, tatu: This is actually unlikely to work ok... but for now, // let's give it a chance? Object delegateValue = convertValue(value); + // consider null (to be consistent with serialize method above) + if (delegateValue == null) { + provider.defaultSerializeNull(gen); + return; + } JsonSerializer ser = _delegateSerializer; if (ser == null) { ser = _findSerializer(value, provider); diff --git a/src/test/java/com/fasterxml/jackson/databind/ser/TestCustomSerializers.java b/src/test/java/com/fasterxml/jackson/databind/ser/TestCustomSerializers.java index 6add817574..4fee0d60e2 100644 --- a/src/test/java/com/fasterxml/jackson/databind/ser/TestCustomSerializers.java +++ b/src/test/java/com/fasterxml/jackson/databind/ser/TestCustomSerializers.java @@ -8,9 +8,7 @@ import org.w3c.dom.Element; -import com.fasterxml.jackson.annotation.JsonFilter; -import com.fasterxml.jackson.annotation.JsonFormat; -import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.core.io.CharacterEscapes; @@ -23,6 +21,8 @@ import com.fasterxml.jackson.databind.ser.std.CollectionSerializer; import com.fasterxml.jackson.databind.ser.std.StdDelegatingSerializer; import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer; +import com.fasterxml.jackson.databind.type.TypeFactory; +import com.fasterxml.jackson.databind.util.Converter; import com.fasterxml.jackson.databind.util.StdConverter; /** @@ -187,6 +187,55 @@ public String getId() { } } + // [databind#4575] + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "@type") + @JsonSubTypes( + { + @JsonSubTypes.Type(Sub4575.class) + } + ) + @JsonTypeName("Super") + static class Super4575 { + public static final Super4575 NULL = new Super4575(); + } + + @JsonTypeName("Sub") + static class Sub4575 extends Super4575 { } + + static class NullSerializer4575 extends StdDelegatingSerializer { + public NullSerializer4575(Converter converter, JavaType delegateType, JsonSerializer delegateSerializer) { + super(converter, delegateType, delegateSerializer); + } + + public NullSerializer4575(TypeFactory typeFactory, JsonSerializer delegateSerializer) { + this( + new Converter() { + @Override + public Object convert(Object value) { + return value == Super4575.NULL ? null : value; + } + + @Override + public JavaType getInputType(TypeFactory typeFactory) { + return typeFactory.constructType(delegateSerializer.handledType()); + } + + @Override + public JavaType getOutputType(TypeFactory typeFactory) { + return typeFactory.constructType(delegateSerializer.handledType()); + } + }, + typeFactory.constructType(delegateSerializer.handledType() == null ? Object.class : delegateSerializer.handledType()), + delegateSerializer + ); + } + + @Override + protected StdDelegatingSerializer withDelegate(Converter converter, JavaType delegateType, JsonSerializer delegateSerializer) { + return new NullSerializer4575(converter, delegateType, delegateSerializer); + } + } + /* /********************************************************** /* Unit tests @@ -208,7 +257,6 @@ public void testCustomization() throws Exception @SuppressWarnings({ "unchecked", "rawtypes" }) public void testCustomLists() throws Exception { - ObjectMapper mapper = new ObjectMapper(); SimpleModule module = new SimpleModule("test", Version.unknownVersion()); JsonSerializer ser = new CollectionSerializer(null, false, null, null); final JsonSerializer collectionSerializer = (JsonSerializer) ser; @@ -225,14 +273,15 @@ public void serialize(Collection value, JsonGenerator gen, SerializerProvider pr } } }); - mapper.registerModule(module); + ObjectMapper mapper = jsonMapperBuilder() + .addModule(module) + .build(); assertEquals("null", mapper.writeValueAsString(new ArrayList())); } // [databind#87]: delegating serializer public void testDelegating() throws Exception { - ObjectMapper mapper = new ObjectMapper(); SimpleModule module = new SimpleModule("test", Version.unknownVersion()); module.addSerializer(new StdDelegatingSerializer(Immutable.class, new StdConverter>() { @@ -245,7 +294,9 @@ public Map convert(Immutable value) return map; } })); - mapper.registerModule(module); + ObjectMapper mapper = jsonMapperBuilder() + .addModule(module) + .build(); assertEquals("{\"x\":3,\"y\":7}", mapper.writeValueAsString(new Immutable())); } @@ -279,8 +330,9 @@ public void testWithCustomElements() throws Exception SimpleModule module = new SimpleModule("test", Version.unknownVersion()); module.addSerializer(String.class, new UCStringSerializer()); - ObjectMapper mapper = new ObjectMapper() - .registerModule(module); + ObjectMapper mapper = jsonMapperBuilder() + .addModule(module) + .build(); assertEquals(q("FOOBAR"), mapper.writeValueAsString("foobar")); assertEquals(a2q("['FOO',null]"), @@ -306,4 +358,28 @@ public void testIssue2475() throws Exception { assertEquals(a2q("{'id':'ID-2','set':[]}"), writer.writeValueAsString(new Item2475(new HashSet(), "ID-2"))); } + + // [databind#4575] + public void testIssue4575() throws Exception { + com.fasterxml.jackson.databind.Module module = new SimpleModule() { + { + setSerializerModifier(new BeanSerializerModifier() { + @Override + public JsonSerializer modifySerializer( + SerializationConfig config, BeanDescription beanDesc, JsonSerializer serializer + ) { + return new NullSerializer4575(config.getTypeFactory(), serializer); + } + }); + } + }; + + ObjectMapper mapper = jsonMapperBuilder() + .addModule(module) + .build(); + + assertEquals("{\"@type\":\"Super\"}", mapper.writeValueAsString(new Super4575())); + assertEquals("{\"@type\":\"Sub\"}", mapper.writeValueAsString(new Sub4575())); + assertEquals("null", mapper.writeValueAsString(Super4575.NULL)); + } }