Skip to content
Draft
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -1428,7 +1431,12 @@ public ValueDeserializer<?> findDefaultDeserializer(DeserializationContext ctxt,
@SuppressWarnings("unchecked")
ValueDeserializer<Object> valueDeser = (ValueDeserializer<Object>) 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.")) {
Expand Down
132 changes: 118 additions & 14 deletions src/main/java/tools/jackson/databind/deser/jdk/MapEntryDeserializer.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -52,6 +62,13 @@ public class MapEntryDeserializer
public MapEntryDeserializer(JavaType type,
KeyDeserializer keyDeser, ValueDeserializer<Object> valueDeser,
TypeDeserializer valueTypeDeser)
{
this(type, keyDeser, valueDeser, valueTypeDeser, false);
}

protected MapEntryDeserializer(JavaType type,
KeyDeserializer keyDeser, ValueDeserializer<Object> valueDeser,
TypeDeserializer valueTypeDeser, boolean deserializeAsPOJO)
{
super(type);
if (type.containedTypeCount() != 2) { // sanity check
Expand All @@ -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,
Expand All @@ -82,6 +88,7 @@ protected MapEntryDeserializer(MapEntryDeserializer src,
_keyDeserializer = keyDeser;
_valueDeserializer = valueDeser;
_valueTypeDeserializer = valueTypeDeser;
_deserializeAsPOJO = src._deserializeAsPOJO;
}

/**
Expand All @@ -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;
Expand All @@ -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);
Expand All @@ -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<Object>) vd, vtd, deserializeAsPOJO);
}
return deser;
}

/*
Expand Down Expand Up @@ -174,6 +203,11 @@ public ValueDeserializer<Object> getContentDeserializer() {
public Map.Entry<Object,Object> 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) {
Expand Down Expand Up @@ -232,6 +266,76 @@ public Map.Entry<Object,Object> deserialize(JsonParser p, DeserializationContext
return new AbstractMap.SimpleEntry<Object,Object>(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<Object,Object> _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<Object,Object>) ctxt.handleUnexpectedToken(getValueType(ctxt), p);
}

final KeyDeserializer keyDes = _keyDeserializer;
Copy link
Member Author

Choose a reason for hiding this comment

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

This is actually wrong: KeyDeserializer is only good for JSON Object property names; here we need a proper ValueDeserializer...

final ValueDeserializer<Object> 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
}

Copy link
Member Author

Choose a reason for hiding this comment

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

Missing checks for missing (absent) "key" or "value"...

if (t != JsonToken.END_OBJECT) {
ctxt.reportInputMismatch(this,
"Problem binding JSON into Map.Entry: unexpected content: "+t);
}

return new AbstractMap.SimpleEntry<Object,Object>(key, value);
}

@Override
public Map.Entry<Object,Object> deserialize(JsonParser p, DeserializationContext ctxt,
Map.Entry<Object,Object> result) throws JacksonException
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package tools.jackson.databind.tofix;
package tools.jackson.databind.format;

import java.util.HashMap;
import java.util.Map;
Expand All @@ -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<String, String> entry;

protected BeanWithMapEntryAsObject() {
protected BeanWithMapEntryAsPOJO() {
}

public BeanWithMapEntryAsObject(String key, String value) {
public BeanWithMapEntryAsPOJO(String key, String value) {
Map<String, String> map = new HashMap<>();
map.put(key, value);
entry = map.entrySet().iterator().next();
Expand All @@ -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());
}
Expand Down