diff --git a/src/main/java/tools/jackson/databind/deser/BasicDeserializerFactory.java b/src/main/java/tools/jackson/databind/deser/BasicDeserializerFactory.java index e7585cd05f..b8fdd1ae27 100644 --- a/src/main/java/tools/jackson/databind/deser/BasicDeserializerFactory.java +++ b/src/main/java/tools/jackson/databind/deser/BasicDeserializerFactory.java @@ -1418,6 +1418,9 @@ public ValueDeserializer findDefaultDeserializer(DeserializationContext ctxt, return createCollectionDeserializer(ctxt, ct, beanDescRef); } if (rawType == CLASS_MAP_ENTRY) { + // [databind#1419]: Check if we should deserialize as POJO instead + JsonFormat.Value format = beanDescRef.findExpectedFormat(Map.Entry.class); + boolean asPOJO = (format.getShape() == JsonFormat.Shape.POJO); // 28-Apr-2015, tatu: TypeFactory does it all for us already so JavaType kt = type.containedTypeOrUnknown(0); JavaType vt = type.containedTypeOrUnknown(1); @@ -1428,7 +1431,12 @@ public ValueDeserializer findDefaultDeserializer(DeserializationContext ctxt, @SuppressWarnings("unchecked") ValueDeserializer valueDeser = (ValueDeserializer) vt.getValueHandler(); KeyDeserializer keyDes = (KeyDeserializer) kt.getValueHandler(); - return new MapEntryDeserializer(type, keyDes, valueDeser, vts); + MapEntryDeserializer meDeser = new MapEntryDeserializer(type, keyDes, valueDeser, vts); + if (asPOJO) { + // !!! 18-Nov-2025, tatu: [databind#1419] TODO -- implement! + ; + } + return meDeser; } String clsName = rawType.getName(); if (rawType.isPrimitive() || clsName.startsWith("java.")) { diff --git a/src/main/java/tools/jackson/databind/deser/jdk/MapEntryDeserializer.java b/src/main/java/tools/jackson/databind/deser/jdk/MapEntryDeserializer.java index a05df30bef..fb352eeda1 100644 --- a/src/main/java/tools/jackson/databind/deser/jdk/MapEntryDeserializer.java +++ b/src/main/java/tools/jackson/databind/deser/jdk/MapEntryDeserializer.java @@ -2,6 +2,8 @@ import java.util.*; +import com.fasterxml.jackson.annotation.JsonFormat; + import tools.jackson.core.*; import tools.jackson.databind.*; import tools.jackson.databind.annotation.JacksonStdImpl; @@ -43,6 +45,14 @@ public class MapEntryDeserializer */ protected final TypeDeserializer _valueTypeDeserializer; + /** + * Flag set when we should deserialize as POJO with "key" and "value" + * properties, instead of the default Map.Entry format. + * + * @since 3.1 (for [databind#1419]) + */ + protected final boolean _deserializeAsPOJO; + /* /********************************************************************** /* Life-cycle @@ -52,6 +62,13 @@ public class MapEntryDeserializer public MapEntryDeserializer(JavaType type, KeyDeserializer keyDeser, ValueDeserializer valueDeser, TypeDeserializer valueTypeDeser) + { + this(type, keyDeser, valueDeser, valueTypeDeser, false); + } + + protected MapEntryDeserializer(JavaType type, + KeyDeserializer keyDeser, ValueDeserializer valueDeser, + TypeDeserializer valueTypeDeser, boolean deserializeAsPOJO) { super(type); if (type.containedTypeCount() != 2) { // sanity check @@ -60,18 +77,7 @@ public MapEntryDeserializer(JavaType type, _keyDeserializer = keyDeser; _valueDeserializer = valueDeser; _valueTypeDeserializer = valueTypeDeser; - } - - /** - * Copy-constructor that can be used by sub-classes to allow - * copy-on-write styling copying of settings of an existing instance. - */ - protected MapEntryDeserializer(MapEntryDeserializer src) - { - super(src); - _keyDeserializer = src._keyDeserializer; - _valueDeserializer = src._valueDeserializer; - _valueTypeDeserializer = src._valueTypeDeserializer; + _deserializeAsPOJO = deserializeAsPOJO; } protected MapEntryDeserializer(MapEntryDeserializer src, @@ -82,6 +88,7 @@ protected MapEntryDeserializer(MapEntryDeserializer src, _keyDeserializer = keyDeser; _valueDeserializer = valueDeser; _valueTypeDeserializer = valueTypeDeser; + _deserializeAsPOJO = src._deserializeAsPOJO; } /** @@ -92,7 +99,6 @@ protected MapEntryDeserializer(MapEntryDeserializer src, protected MapEntryDeserializer withResolved(KeyDeserializer keyDeser, TypeDeserializer valueTypeDeser, ValueDeserializer valueDeser) { - if ((_keyDeserializer == keyDeser) && (_valueDeserializer == valueDeser) && (_valueTypeDeserializer == valueTypeDeser)) { return this; @@ -117,10 +123,27 @@ public LogicalType logicalType() { * Method called to finalize setup of this deserializer, * when it is known for which property deserializer is needed for. */ + @SuppressWarnings("unchecked") @Override public ValueDeserializer createContextual(DeserializationContext ctxt, BeanProperty property) { + // [databind#1419]: Check if property has @JsonFormat(shape=POJO) + boolean deserializeAsPOJO = _deserializeAsPOJO; + if (property != null) { + JsonFormat.Value format = property.findPropertyFormat(ctxt.getConfig(), Map.Entry.class); + + switch (format.getShape()) { + case NATURAL: + deserializeAsPOJO = false; + break; + case POJO: + deserializeAsPOJO = true; + break; + default: // fall through + } + } + KeyDeserializer kd = _keyDeserializer; if (kd == null) { kd = ctxt.findKeyDeserializer(_containerType.containedType(0), property); @@ -141,7 +164,13 @@ public ValueDeserializer createContextual(DeserializationContext ctxt, if (vtd != null) { vtd = vtd.forProperty(property); } - return withResolved(kd, vtd, vd); + + MapEntryDeserializer deser = withResolved(kd, vtd, vd); + if (deserializeAsPOJO != _deserializeAsPOJO) { + return new MapEntryDeserializer(_containerType, kd, + (ValueDeserializer) vd, vtd, deserializeAsPOJO); + } + return deser; } /* @@ -174,6 +203,11 @@ public ValueDeserializer getContentDeserializer() { public Map.Entry deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException { + // [databind#1419]: If deserializing as POJO with "key" and "value" properties + if (_deserializeAsPOJO) { + return _deserializeAsPOJO(p, ctxt); + } + // Ok: must point to START_OBJECT, PROPERTY_NAME or END_OBJECT JsonToken t = p.currentToken(); if (t == JsonToken.START_OBJECT) { @@ -232,6 +266,76 @@ public Map.Entry deserialize(JsonParser p, DeserializationContext return new AbstractMap.SimpleEntry(key, value); } + /** + * Helper method to deserialize Map.Entry as POJO with "key" and "value" properties. + * + * @since 3.1 (for [databind#1419]) + */ + @SuppressWarnings("unchecked") + protected Map.Entry _deserializeAsPOJO(JsonParser p, DeserializationContext ctxt) + throws JacksonException + { + JsonToken t = p.currentToken(); + if (t == JsonToken.START_OBJECT) { + t = p.nextToken(); + } else if (t != JsonToken.PROPERTY_NAME && t != JsonToken.END_OBJECT) { + if (t == JsonToken.START_ARRAY) { + return _deserializeFromArray(p, ctxt); + } + return (Map.Entry) ctxt.handleUnexpectedToken(getValueType(ctxt), p); + } + + final KeyDeserializer keyDes = _keyDeserializer; + final ValueDeserializer valueDes = _valueDeserializer; + final TypeDeserializer typeDeser = _valueTypeDeserializer; + + Object key = null; + Object value = null; + + // Read properties "key" and "value" + while (t == JsonToken.PROPERTY_NAME) { + String propName = p.currentName(); + t = p.nextToken(); // move to value + + if ("key".equals(propName)) { + // Deserialize key + if (t == JsonToken.VALUE_NULL) { + key = keyDes.deserializeKey(null, ctxt); + } else if (t.isScalarValue()) { + key = keyDes.deserializeKey(p.getString(), ctxt); + } else { + ctxt.reportInputMismatch(this, + "Can not deserialize Map.Entry key from non-scalar JSON value"); + } + } else if ("value".equals(propName)) { + // Deserialize value + try { + if (t == JsonToken.VALUE_NULL) { + value = valueDes.getNullValue(ctxt); + } else if (typeDeser == null) { + value = valueDes.deserialize(p, ctxt); + } else { + value = valueDes.deserializeWithType(p, ctxt, typeDeser); + } + } catch (Exception e) { + wrapAndThrow(ctxt, e, Map.Entry.class, propName); + } + } else { + // Unknown property: check if we should fail or skip + handleUnknownProperty(p, ctxt, _containerType.getRawClass(), propName); + } + + t = p.nextToken(); // move to next property or END_OBJECT + } + + if (t != JsonToken.END_OBJECT) { + ctxt.reportInputMismatch(this, + "Problem binding JSON into Map.Entry: unexpected content: "+t); + } + + return new AbstractMap.SimpleEntry(key, value); + } + @Override public Map.Entry deserialize(JsonParser p, DeserializationContext ctxt, Map.Entry result) throws JacksonException diff --git a/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java b/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java index e04eae4f18..4f596a06b7 100644 --- a/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java +++ b/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java @@ -4,7 +4,6 @@ import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.reflect.Field; -import java.lang.reflect.Method; import java.util.HashMap; import com.fasterxml.jackson.annotation.JsonInclude; diff --git a/src/test/java/tools/jackson/databind/tofix/MapEntryFormat1419Test.java b/src/test/java/tools/jackson/databind/format/MapEntryFormat1419Test.java similarity index 60% rename from src/test/java/tools/jackson/databind/tofix/MapEntryFormat1419Test.java rename to src/test/java/tools/jackson/databind/format/MapEntryFormat1419Test.java index d1efd80860..26788584d4 100644 --- a/src/test/java/tools/jackson/databind/tofix/MapEntryFormat1419Test.java +++ b/src/test/java/tools/jackson/databind/format/MapEntryFormat1419Test.java @@ -1,4 +1,4 @@ -package tools.jackson.databind.tofix; +package tools.jackson.databind.format; import java.util.HashMap; import java.util.Map; @@ -9,20 +9,19 @@ 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; // for [databind#1419] -class MapEntryFormat1419Test extends DatabindTestUtil { - static class BeanWithMapEntryAsObject { - @JsonFormat(shape = JsonFormat.Shape.OBJECT) +public class MapEntryFormat1419Test extends DatabindTestUtil { + static class BeanWithMapEntryAsPOJO { + @JsonFormat(shape = JsonFormat.Shape.POJO) public Map.Entry entry; - protected BeanWithMapEntryAsObject() { + protected BeanWithMapEntryAsPOJO() { } - public BeanWithMapEntryAsObject(String key, String value) { + public BeanWithMapEntryAsPOJO(String key, String value) { Map map = new HashMap<>(); map.put(key, value); entry = map.entrySet().iterator().next(); @@ -31,13 +30,12 @@ public BeanWithMapEntryAsObject(String key, String value) { private final ObjectMapper MAPPER = newJsonMapper(); - @JacksonTestFailureExpected @Test void wrappedAsObjectRoundtrip() throws Exception { - BeanWithMapEntryAsObject input = new BeanWithMapEntryAsObject("foo", "bar"); + BeanWithMapEntryAsPOJO input = new BeanWithMapEntryAsPOJO("foo", "bar"); String json = MAPPER.writeValueAsString(input); assertEquals(a2q("{'entry':{'key':'foo','value':'bar'}}"), json); - BeanWithMapEntryAsObject result = MAPPER.readValue(json, BeanWithMapEntryAsObject.class); + BeanWithMapEntryAsPOJO result = MAPPER.readValue(json, BeanWithMapEntryAsPOJO.class); assertEquals("foo", result.entry.getKey()); assertEquals("bar", result.entry.getValue()); }