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 diff --git a/src/main/java/tools/jackson/databind/SerializationContext.java b/src/main/java/tools/jackson/databind/SerializationContext.java index 36e75c3007..6514e53ccb 100644 --- a/src/main/java/tools/jackson/databind/SerializationContext.java +++ b/src/main/java/tools/jackson/databind/SerializationContext.java @@ -695,6 +695,10 @@ public ValueSerializer findPrimaryPropertySerializer(JavaType valueType, ValueSerializer ser = _knownSerializers.untypedValueSerializer(valueType); if (ser == null) { ser = _createAndCachePropertySerializer(valueType, property); + } else if (property != null) { + 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); } @@ -705,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); } @@ -1041,7 +1057,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) { diff --git a/src/test/java/tools/jackson/databind/tofix/MapFormatShape5405Test.java b/src/test/java/tools/jackson/databind/format/MapFormatShape5405Test.java similarity index 83% rename from src/test/java/tools/jackson/databind/tofix/MapFormatShape5405Test.java rename to src/test/java/tools/jackson/databind/format/MapFormatShape5405Test.java index 708699f003..2d81120656 100644 --- a/src/test/java/tools/jackson/databind/tofix/MapFormatShape5405Test.java +++ b/src/test/java/tools/jackson/databind/format/MapFormatShape5405Test.java @@ -1,4 +1,4 @@ -package tools.jackson.databind.tofix; +package tools.jackson.databind.format; import java.util.*; @@ -8,9 +8,8 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonPropertyOrder; -import tools.jackson.databind.*; +import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.testutil.DatabindTestUtil; -import tools.jackson.databind.testutil.failure.JacksonTestFailureExpected; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -72,9 +71,20 @@ 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(