diff --git a/src/main/java/com/fasterxml/jackson/databind/BeanDescription.java b/src/main/java/com/fasterxml/jackson/databind/BeanDescription.java index d50b0f858d..2426c134e8 100644 --- a/src/main/java/com/fasterxml/jackson/databind/BeanDescription.java +++ b/src/main/java/com/fasterxml/jackson/databind/BeanDescription.java @@ -165,6 +165,16 @@ protected BeanDescription(JavaType type) { */ public abstract AnnotatedMethod findAnySetter(); + /** + * Method used to locate the field of the class that implements + * {@link com.fasterxml.jackson.annotation.JsonAnySetter} If no such method + * exists null is returned. If more than one are found, an exception is + * thrown. + * + * @since 2.8 + */ + public abstract AnnotatedMember findAnySetterField(); + /** * Method for locating the getter method that is annotated with * {@link com.fasterxml.jackson.annotation.JsonValue} annotation, diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java index 83927e993a..58e1856f1d 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java @@ -469,13 +469,20 @@ protected void addBeanProps(DeserializationContext ctxt, } // Also, do we have a fallback "any" setter? - AnnotatedMethod anySetter = beanDesc.findAnySetter(); - if (anySetter != null) { - builder.setAnySetter(constructAnySetter(ctxt, beanDesc, anySetter)); + AnnotatedMethod anySetterMethod = beanDesc.findAnySetter(); + AnnotatedMember anySetterField = null; + if (anySetterMethod != null) { + builder.setAnySetter(constructAnySetter(ctxt, beanDesc, anySetterMethod)); + } + else { + anySetterField = beanDesc.findAnySetterField(); + if(anySetterField != null) { + builder.setAnySetter(constructAnySetter(ctxt, beanDesc, anySetterField)); + } } // NOTE: we do NOT add @JsonIgnore'd properties into blocked ones if there's any-setter // Implicit ones via @JsonIgnore and equivalent? - if (anySetter == null) { + if (anySetterMethod == null && anySetterField == null) { Collection ignored2 = beanDesc.getIgnoredPropertyNames(); if (ignored2 != null) { for (String propName : ignored2) { @@ -670,14 +677,23 @@ protected void addInjectables(DeserializationContext ctxt, * has been designated as such setter. */ protected SettableAnyProperty constructAnySetter(DeserializationContext ctxt, - BeanDescription beanDesc, AnnotatedMethod setter) + BeanDescription beanDesc, AnnotatedMember setter) throws JsonMappingException { if (ctxt.canOverrideAccessModifiers()) { setter.fixAccess(ctxt.isEnabled(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS)); // to ensure we can call it } - // we know it's a 2-arg method, second arg is the value - JavaType type = setter.getParameterType(1); + + //find the java type based on the annotated setter method or setter field + JavaType type = null; + if (setter instanceof AnnotatedMethod) { + // we know it's a 2-arg method, second arg is the value + type = ((AnnotatedMethod) setter).getParameterType(1); + } else if (setter instanceof AnnotatedField) { + // get the type from the content type of the map object + type = ((AnnotatedField) setter).getType().getContentType(); + } + BeanProperty.Std property = new BeanProperty.Std(PropertyName.construct(setter.getName()), type, null, beanDesc.getClassAnnotations(), setter, PropertyMetadata.STD_OPTIONAL); diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/SettableAnyProperty.java b/src/main/java/com/fasterxml/jackson/databind/deser/SettableAnyProperty.java index 92261be789..58ef1d5f3c 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/SettableAnyProperty.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/SettableAnyProperty.java @@ -1,10 +1,14 @@ package com.fasterxml.jackson.databind.deser; import java.io.IOException; +import java.util.HashMap; +import java.util.Map; import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.deser.impl.ReadableObjectId.Referring; +import com.fasterxml.jackson.databind.introspect.AnnotatedField; +import com.fasterxml.jackson.databind.introspect.AnnotatedMember; import com.fasterxml.jackson.databind.introspect.AnnotatedMethod; import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; @@ -30,8 +34,8 @@ public class SettableAnyProperty /** * Annotated variant is needed for JDK serialization only */ - final protected AnnotatedMethod _setter; - + final protected AnnotatedMember _setter; + protected final JavaType _type; protected JsonDeserializer _valueDeserializer; @@ -44,7 +48,7 @@ public class SettableAnyProperty /********************************************************** */ - public SettableAnyProperty(BeanProperty property, AnnotatedMethod setter, JavaType type, + public SettableAnyProperty(BeanProperty property, AnnotatedMember setter, JavaType type, JsonDeserializer valueDeser, TypeDeserializer typeDeser) { _property = property; @@ -53,7 +57,7 @@ public SettableAnyProperty(BeanProperty property, AnnotatedMethod setter, JavaTy _valueDeserializer = valueDeser; _valueTypeDeserializer = typeDeser; } - + public SettableAnyProperty withValueDeserializer(JsonDeserializer deser) { return new SettableAnyProperty(_property, _setter, _type, deser, _valueTypeDeserializer); @@ -138,11 +142,29 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOEx return _valueDeserializer.deserialize(p, ctxt); } + @SuppressWarnings("unchecked") public void set(Object instance, String propName, Object value) throws IOException { try { - // note: can not use 'setValue()' due to taking 2 args - _setter.getAnnotated().invoke(instance, propName, value); + // note: can not use 'setValue()' due to taking 2 args + if (_setter instanceof AnnotatedMethod) { + ((AnnotatedMethod) _setter).getAnnotated().invoke(instance, propName, value); + } + // if annotation in the field (only map is supported now) + else if (_setter instanceof AnnotatedField) { + AnnotatedField field = ((AnnotatedField) _setter); + + // get the field value.. + Object val = field.getValue(instance); + + if (val != null) { + // add the property key and value + ((Map) val).put(propName, value); + + // set the value back to the field + field.setValue(instance, val); + } + } } catch (Exception e) { _throwAsIOE(e, propName, value); } diff --git a/src/main/java/com/fasterxml/jackson/databind/introspect/BasicBeanDescription.java b/src/main/java/com/fasterxml/jackson/databind/introspect/BasicBeanDescription.java index d0b482503c..2b50840c33 100644 --- a/src/main/java/com/fasterxml/jackson/databind/introspect/BasicBeanDescription.java +++ b/src/main/java/com/fasterxml/jackson/databind/introspect/BasicBeanDescription.java @@ -418,6 +418,22 @@ public AnnotatedMember findAnyGetter() throws IllegalArgumentException } return anyGetter; } + + public AnnotatedMember findAnySetterField() throws IllegalArgumentException { + AnnotatedMember anySetter = (_propCollector == null) ? null : _propCollector.getAnySetterField(); + if (anySetter != null) { + /* + * For now let's require a Map; in future can add support for other + * types like perhaps Iterable? + */ + Class type = anySetter.getRawType(); + if (!Map.class.isAssignableFrom(type)) { + throw new IllegalArgumentException("Invalid 'any-setter' annotation on field " + anySetter.getName() + + "(): type is not instance of java.util.Map"); + } + } + return anySetter; + } @Override public Map findBackReferenceProperties() diff --git a/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertiesCollector.java b/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertiesCollector.java index 0590a2697f..5133a0f901 100644 --- a/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertiesCollector.java +++ b/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertiesCollector.java @@ -3,6 +3,7 @@ import java.lang.reflect.Modifier; import java.util.*; +import com.fasterxml.jackson.annotation.JsonAnySetter; import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.cfg.HandlerInstantiator; import com.fasterxml.jackson.databind.cfg.MapperConfig; @@ -83,6 +84,8 @@ public class POJOPropertiesCollector protected LinkedList _anyGetters; protected LinkedList _anySetters; + + protected LinkedList _anySetterField; /** * Method(s) marked with 'JsonValue' annotation @@ -194,6 +197,21 @@ public AnnotatedMember getAnyGetter() } return null; } + + public AnnotatedMember getAnySetterField() + { + if (!_collected) { + collectAll(); + } + if (_anySetterField != null) { + if (_anySetterField.size() > 1) { + reportProblem("Multiple 'any-Setters' defined ("+_anySetters.get(0)+" vs " + +_anySetterField.get(1)+")"); + } + return _anySetterField.getFirst(); + } + return null; + } public AnnotatedMethod getAnySetterMethod() { @@ -401,6 +419,15 @@ protected void _addFields(Map props) if (pruneFinalFields && (pn == null) && !ignored && Modifier.isFinal(f.getModifiers())) { continue; } + + //if field has annotation @JsonAnySetter + if(f.hasAnnotation(JsonAnySetter.class)) { + if (_anySetterField == null) { + _anySetterField = new LinkedList(); + } + _anySetterField.add(f); + } + _property(props, implName).addField(f, pn, nameExplicit, visible, ignored); } } diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/TestAnyProperties.java b/src/test/java/com/fasterxml/jackson/databind/deser/TestAnyProperties.java index 5b8c104694..fb7ef8e607 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/TestAnyProperties.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/TestAnyProperties.java @@ -135,6 +135,33 @@ public void prop(String name, Base value) { } } + static class JsonAnySetterOnMap { + public int id; + + @JsonAnySetter + protected HashMap other = new HashMap(); + + @JsonAnyGetter + public Map any() { + return other; + } + + } + + static class JsonAnySetterOnNullMap { + public int id; + + @JsonAnySetter + protected HashMap other; + + @JsonAnyGetter + public Map any() { + return other; + } + + } + + /* /********************************************************** /* Test methods @@ -224,6 +251,21 @@ public void testPolymorphic() throws Exception assertEquals("xyz", ((Impl) ob).value); } + public void testJsonAnySetterOnMap() throws Exception { + JsonAnySetterOnMap result = MAPPER.readValue("{\"id\":2,\"name\":\"Joe\", \"city\":\"New Jersey\"}", + JsonAnySetterOnMap.class); + assertEquals(2, result.id); + assertEquals("Joe", result.other.get("name")); + assertEquals("New Jersey", result.other.get("city")); + } + + public void testJsonAnySetterOnNullMap() throws Exception { + JsonAnySetterOnNullMap result = MAPPER.readValue("{\"id\":2,\"name\":\"Joe\", \"city\":\"New Jersey\"}", + JsonAnySetterOnNullMap.class); + assertEquals(2, result.id); + assertNull(result.other); + } + /* /********************************************************** /* Private helper methods