From 2a15b6c9fc12906c8b5932def7ada3df8b3c6ff9 Mon Sep 17 00:00:00 2001 From: Jackson Date: Mon, 1 Dec 2025 21:11:33 +0900 Subject: [PATCH 1/7] Fix #5405, @JsonFormat(shape = Shape.POJO) does not work for java.util.Map serialization via property annotation --- .../jackson/databind/SerializationContext.java | 7 +++++++ .../{tofix => }/MapFormatShape5405Test.java | 18 +++++++++++++----- 2 files changed, 20 insertions(+), 5 deletions(-) rename src/test/java/tools/jackson/databind/{tofix => }/MapFormatShape5405Test.java (81%) diff --git a/src/main/java/tools/jackson/databind/SerializationContext.java b/src/main/java/tools/jackson/databind/SerializationContext.java index 36e75c3007..7fab322bc1 100644 --- a/src/main/java/tools/jackson/databind/SerializationContext.java +++ b/src/main/java/tools/jackson/databind/SerializationContext.java @@ -695,6 +695,13 @@ public ValueSerializer findPrimaryPropertySerializer(JavaType valueType, ValueSerializer ser = _knownSerializers.untypedValueSerializer(valueType); if (ser == null) { ser = _createAndCachePropertySerializer(valueType, property); + } else if (property != null) { + // [databind#5405]: property-level @JsonFormat overrides must be applied even with cached serializers + JsonFormat.Value overrides = property.findFormatOverrides(_config); + if (overrides != null && !overrides.equals(JsonFormat.Value.empty())) { + BeanDescription.Supplier beanDescRef = lazyIntrospectBeanDescription(valueType); + ser = _checkShapeShifting(valueType, beanDescRef, property, ser); + } } return handlePrimaryContextualization(ser, property); } diff --git a/src/test/java/tools/jackson/databind/tofix/MapFormatShape5405Test.java b/src/test/java/tools/jackson/databind/MapFormatShape5405Test.java similarity index 81% rename from src/test/java/tools/jackson/databind/tofix/MapFormatShape5405Test.java rename to src/test/java/tools/jackson/databind/MapFormatShape5405Test.java index 708699f003..1957b33635 100644 --- a/src/test/java/tools/jackson/databind/tofix/MapFormatShape5405Test.java +++ b/src/test/java/tools/jackson/databind/MapFormatShape5405Test.java @@ -1,4 +1,4 @@ -package tools.jackson.databind.tofix; +package tools.jackson.databind; import java.util.*; @@ -8,9 +8,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonPropertyOrder; -import tools.jackson.databind.*; import tools.jackson.databind.testutil.DatabindTestUtil; -import tools.jackson.databind.testutil.failure.JacksonTestFailureExpected; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -72,13 +70,23 @@ public Bean5405Override(int value) { // [databind#5045]: property overrides for @JsonFormat.shape won't work for Maps // 30-Nov-2025, tatu: Something about caching is the issue: if "b" commented out, // override appears to work; with "b" not - @JacksonTestFailureExpected @Test public void serializeAsPOJOViaProperty() throws Exception + { + String result = MAPPER.writeValueAsString(new Bean5405Container(1,0,3)); + assertEquals(a2q( + "{'a':{'extra':13,'empty':false},'c':{'extra':13,'empty':false}}"), + result); + } + + // [databind#5405]: + // 01-Dec-2025, JacksonJang: In this case, the @JsonFormat(shape = POJO) override behaves correctly even with b included. + @Test + public void serializeAsPOJOViaFullProperty() throws Exception { String result = MAPPER.writeValueAsString(new Bean5405Container(1,2,3)); assertEquals(a2q( - "{'a':{'extra':13,'empty':false},'b':{'value':2},'c':{'extra':13,'empty':false}}"), + "{'a':{'extra':13,'empty':false},'b':{'value':2},'c':{'extra':13,'empty':false}}"), result); } From 046dedd39115dc06d358cb74a3ae9588e1c007f1 Mon Sep 17 00:00:00 2001 From: Jackson Date: Sat, 6 Dec 2025 21:40:59 +0900 Subject: [PATCH 2/7] Fix #5405: Apply property-level JsonFormat overrides for cached serializers --- .../tools/jackson/databind/SerializationContext.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/main/java/tools/jackson/databind/SerializationContext.java b/src/main/java/tools/jackson/databind/SerializationContext.java index 7fab322bc1..3555da0b68 100644 --- a/src/main/java/tools/jackson/databind/SerializationContext.java +++ b/src/main/java/tools/jackson/databind/SerializationContext.java @@ -696,12 +696,9 @@ public ValueSerializer findPrimaryPropertySerializer(JavaType valueType, if (ser == null) { ser = _createAndCachePropertySerializer(valueType, property); } else if (property != null) { - // [databind#5405]: property-level @JsonFormat overrides must be applied even with cached serializers - JsonFormat.Value overrides = property.findFormatOverrides(_config); - if (overrides != null && !overrides.equals(JsonFormat.Value.empty())) { - BeanDescription.Supplier beanDescRef = lazyIntrospectBeanDescription(valueType); - ser = _checkShapeShifting(valueType, beanDescRef, property, ser); - } + BeanDescription.Supplier beanDescRef = lazyIntrospectBeanDescription(valueType); + // [databind#5405]: property-level @JsonFormat must be honored even with cached serializers + ser = _checkShapeShifting(valueType, beanDescRef, property, ser); } return handlePrimaryContextualization(ser, property); } From 662d05058f0bc26ec92a625d8c7c1572d8ce63d2 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 12 Dec 2025 18:11:59 -0800 Subject: [PATCH 3/7] Move test to optimal package --- .../jackson/databind/{ => format}/MapFormatShape5405Test.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) rename src/test/java/tools/jackson/databind/{ => format}/MapFormatShape5405Test.java (97%) diff --git a/src/test/java/tools/jackson/databind/MapFormatShape5405Test.java b/src/test/java/tools/jackson/databind/format/MapFormatShape5405Test.java similarity index 97% rename from src/test/java/tools/jackson/databind/MapFormatShape5405Test.java rename to src/test/java/tools/jackson/databind/format/MapFormatShape5405Test.java index 1957b33635..e70b8e74dd 100644 --- a/src/test/java/tools/jackson/databind/MapFormatShape5405Test.java +++ b/src/test/java/tools/jackson/databind/format/MapFormatShape5405Test.java @@ -1,4 +1,4 @@ -package tools.jackson.databind; +package tools.jackson.databind.format; import java.util.*; @@ -8,6 +8,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.testutil.DatabindTestUtil; import static org.junit.jupiter.api.Assertions.assertEquals; From 4f59606feb2466b930f24ee1a4da1e3d58920845 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 12 Dec 2025 18:14:46 -0800 Subject: [PATCH 4/7] Add release notes --- release-notes/CREDITS | 7 +++++-- release-notes/VERSION | 5 ++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/release-notes/CREDITS b/release-notes/CREDITS index f74eac913f..8b6740471a 100644 --- a/release-notes/CREDITS +++ b/release-notes/CREDITS @@ -125,10 +125,13 @@ Oliver Drotbohm (@odrotbohm) @JacksonJang * Contributed fix for #4629: `@JsonIncludeProperties` and `@JsonIgnoreProperties` - ignored when deserializing Records + ignored when deserializing Records [3.1.0] * Contributed fix for #5115: `@JsonUnwrapped` Record deserialization can't handle - name collision + name collision + [3.1.0] + * Contributed fix for #5405: `@JsonFormat(shape = Shape.POJO)` does not work for + `java.util.Map` serialization via property annotation [3.1.0] Viktor Szathmáry (@phraktle) diff --git a/release-notes/VERSION b/release-notes/VERSION index eec9fd85c2..467f59d775 100644 --- a/release-notes/VERSION +++ b/release-notes/VERSION @@ -39,9 +39,12 @@ Versions: 3.x (for earlier see VERSION-2.x) #5361: Fix Maven SBOM publishing (worked in 3.0.0-rc4 but not in rc5 or later) (date-time)#359: `InstantDeserializer` deserializes the nanosecond portion of fractional negative timestamps incorrectly +#5405: `@JsonFormat(shape = Shape.POJO)` does not work for `java.util.Map` + serialization via property annotation + (fix contributed by @JacksonJang) #5413: Add/support forward reference resolution for array values (contributed by Hélios G) -5442: Make `JsonMapper/ObjectMapper` fully proxyable by CGLIB +#5442: Make `JsonMapper/ObjectMapper` fully proxyable by CGLIB (fix contributed by Fouad A) #5456: Additional configuration (`JsonNodeFeature.STRIP_TRAILING_BIGDECIMAL_ZEROES`: true) to MapperBuilder#configureForJackson2 to closer match Jackson 2 behavior From b40b522fc65a1babe448fecb9d79cd012229f314 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 12 Dec 2025 18:45:47 -0800 Subject: [PATCH 5/7] tiny comment re-formatting --- .../jackson/databind/format/MapFormatShape5405Test.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/test/java/tools/jackson/databind/format/MapFormatShape5405Test.java b/src/test/java/tools/jackson/databind/format/MapFormatShape5405Test.java index e70b8e74dd..2d81120656 100644 --- a/src/test/java/tools/jackson/databind/format/MapFormatShape5405Test.java +++ b/src/test/java/tools/jackson/databind/format/MapFormatShape5405Test.java @@ -81,13 +81,14 @@ public void serializeAsPOJOViaProperty() throws Exception } // [databind#5405]: - // 01-Dec-2025, JacksonJang: In this case, the @JsonFormat(shape = POJO) override behaves correctly even with b included. + // 01-Dec-2025, JacksonJang: In this case, the @JsonFormat(shape = POJO) override + // behaves correctly even with b included. @Test public void serializeAsPOJOViaFullProperty() throws Exception { String result = MAPPER.writeValueAsString(new Bean5405Container(1,2,3)); assertEquals(a2q( - "{'a':{'extra':13,'empty':false},'b':{'value':2},'c':{'extra':13,'empty':false}}"), + "{'a':{'extra':13,'empty':false},'b':{'value':2},'c':{'extra':13,'empty':false}}"), result); } From eafd1c75b3327d67481ac298e9a71aa3e85f2e38 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 12 Dec 2025 19:10:35 -0800 Subject: [PATCH 6/7] ... --- src/main/java/tools/jackson/databind/SerializationContext.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/tools/jackson/databind/SerializationContext.java b/src/main/java/tools/jackson/databind/SerializationContext.java index 3555da0b68..c7704e7da1 100644 --- a/src/main/java/tools/jackson/databind/SerializationContext.java +++ b/src/main/java/tools/jackson/databind/SerializationContext.java @@ -1045,7 +1045,7 @@ private ValueSerializer _checkShapeShifting(JavaType type, BeanDescription.Supplier beanDescRef, BeanProperty prop, ValueSerializer ser) { JsonFormat.Value overrides = prop.findFormatOverrides(_config); - if (overrides != null) { + if (overrides != null && overrides != JsonFormat.Value.empty()) { // First: it may be completely fine to use serializer, despite some overrides ValueSerializer ser2 = ser.withFormatOverrides(_config, overrides); if (ser2 != null) { From dfc0978c0835dc17ecdcaf1a41e760d4df1f6493 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 12 Dec 2025 19:23:45 -0800 Subject: [PATCH 7/7] Add shape-shifting-check to the other code path. --- .../jackson/databind/SerializationContext.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/main/java/tools/jackson/databind/SerializationContext.java b/src/main/java/tools/jackson/databind/SerializationContext.java index c7704e7da1..6514e53ccb 100644 --- a/src/main/java/tools/jackson/databind/SerializationContext.java +++ b/src/main/java/tools/jackson/databind/SerializationContext.java @@ -709,14 +709,26 @@ public ValueSerializer findPrimaryPropertySerializer(JavaType valueType, public ValueSerializer findPrimaryPropertySerializer(Class rawType, BeanProperty property) { + boolean checkShape = (property != null); + JavaType fullType = null; + ValueSerializer ser = _knownSerializers.untypedValueSerializer(rawType); if (ser == null) { - JavaType fullType = _config.constructType(rawType); + fullType = _config.constructType(rawType); ser = _serializerCache.untypedValueSerializer(fullType); if (ser == null) { + checkShape = false; // because next call does it ser = _createAndCachePropertySerializer(rawType, fullType, property); } } + if (checkShape) { + if (fullType == null) { + fullType = _config.constructType(rawType); + } + BeanDescription.Supplier beanDescRef = lazyIntrospectBeanDescription(fullType); + // [databind#5405]: property-level @JsonFormat must be honored even with cached serializers + ser = _checkShapeShifting(fullType, beanDescRef, property, ser); + } return handlePrimaryContextualization(ser, property); }