diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/AnyGetterWriter.java b/src/main/java/com/fasterxml/jackson/databind/ser/AnyGetterWriter.java index bfa85185f3..3bfa7b2849 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/AnyGetterWriter.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/AnyGetterWriter.java @@ -60,7 +60,8 @@ public void getAndFilter(Object bean, JsonGenerator jgen, SerializerProvider pro throw new JsonMappingException("Value returned by 'any-getter' (" +_accessor.getName()+"()) not java.util.Map but "+value.getClass().getName()); } - _serializer.serializeFilteredFields((Map) value, jgen, provider, filter); + // 19-Oct-2014, tatu: Should we try to support @JsonInclude options here? + _serializer.serializeFilteredFields((Map) value, jgen, provider, filter, null); } // Note: NOT part of ResolvableSerializer... diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/BasicSerializerFactory.java b/src/main/java/com/fasterxml/jackson/databind/ser/BasicSerializerFactory.java index 0a85b8b819..16fac48e89 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/BasicSerializerFactory.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/BasicSerializerFactory.java @@ -9,6 +9,8 @@ import java.util.*; import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonInclude; + import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.databind.cfg.SerializerFactoryConfig; @@ -20,10 +22,7 @@ import com.fasterxml.jackson.databind.ser.impl.*; import com.fasterxml.jackson.databind.ser.std.*; import com.fasterxml.jackson.databind.type.*; -import com.fasterxml.jackson.databind.util.ClassUtil; -import com.fasterxml.jackson.databind.util.Converter; -import com.fasterxml.jackson.databind.util.EnumValues; -import com.fasterxml.jackson.databind.util.TokenBuffer; +import com.fasterxml.jackson.databind.util.*; /** * Factory class that can provide serializers for standard JDK classes, @@ -735,9 +734,15 @@ protected JsonSerializer buildMapSerializer(SerializationConfig config, elementTypeSerializer, elementValueSerializer); } else { Object filterId = findFilterId(config, beanDesc); - ser = MapSerializer.construct(config.getAnnotationIntrospector().findPropertiesToIgnore(beanDesc.getClassInfo()), + MapSerializer mapSer = MapSerializer.construct(config.getAnnotationIntrospector().findPropertiesToIgnore(beanDesc.getClassInfo()), type, staticTyping, elementTypeSerializer, keySerializer, elementValueSerializer, filterId); + Object suppressableValue = findSuppressableContentValue(config, + type.getContentType(), beanDesc); + if (suppressableValue != null) { + mapSer = mapSer.withContentInclusion(suppressableValue); + } + ser = mapSer; } } // [Issue#120]: Allow post-processing @@ -749,6 +754,30 @@ protected JsonSerializer buildMapSerializer(SerializationConfig config, return ser; } + /** + * @since 2.5 + */ + protected Object findSuppressableContentValue(SerializationConfig config, + JavaType contentType, BeanDescription beanDesc) + throws JsonMappingException + { + JsonInclude.Include incl = beanDesc.findSerializationInclusionForContent(null); + + if (incl != null) { + switch (incl) { + case NON_DEFAULT: + // 19-Oct-2014, tatu: Not sure what this'd mean; so take it to mean "NON_EMPTY"... + incl = JsonInclude.Include.NON_EMPTY; + break; + default: + // all other modes actually good as is, unless we'll find better ways + break; + } + return incl; + } + return null; + } + /* /********************************************************** /* Factory methods, for Arrays diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/BeanPropertyWriter.java b/src/main/java/com/fasterxml/jackson/databind/ser/BeanPropertyWriter.java index 00c545d6d4..1811c7d6b2 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/BeanPropertyWriter.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/BeanPropertyWriter.java @@ -6,6 +6,7 @@ import java.lang.reflect.Type; import java.util.HashMap; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.SerializableString; import com.fasterxml.jackson.core.io.SerializedString; @@ -37,8 +38,8 @@ public class BeanPropertyWriter extends PropertyWriter /** * Marker object used to indicate "do not serialize if empty" */ - public final static Object MARKER_FOR_EMPTY = new Object(); - + public final static Object MARKER_FOR_EMPTY = JsonInclude.Include.NON_EMPTY; + /* /********************************************************** /* Settings for accessing property value to serialize diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/std/MapSerializer.java b/src/main/java/com/fasterxml/jackson/databind/ser/std/MapSerializer.java index c89324818c..c9a0b5dc9e 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/std/MapSerializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/std/MapSerializer.java @@ -4,6 +4,7 @@ import java.lang.reflect.Type; import java.util.*; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.annotation.JacksonStdImpl; @@ -17,6 +18,7 @@ import com.fasterxml.jackson.databind.ser.PropertyFilter; import com.fasterxml.jackson.databind.ser.impl.PropertySerializerMap; import com.fasterxml.jackson.databind.type.TypeFactory; +import com.fasterxml.jackson.databind.util.ArrayBuilders; /** * Standard serializer implementation for serializing {link java.util.Map} types. @@ -30,12 +32,12 @@ public class MapSerializer implements ContextualSerializer { protected final static JavaType UNSPECIFIED_TYPE = TypeFactory.unknownType(); - + /** * Map-valued property being serialized with this instance */ protected final BeanProperty _property; - + /** * Set of entries to omit during serialization, if any */ @@ -92,6 +94,15 @@ public class MapSerializer * @since 2.4 */ protected final boolean _sortKeys; + + /** + * Value that indicates suppression mechanism to use; either one of + * values of {@link JsonInclude.Include}, or actual object to compare + * against ("default value") + * + * @since 2.5 + */ + protected final Object _suppressableValue; /* /********************************************************** @@ -99,6 +110,9 @@ public class MapSerializer /********************************************************** */ + /** + * @since 2.5 + */ @SuppressWarnings("unchecked") protected MapSerializer(HashSet ignoredEntries, JavaType keyType, JavaType valueType, boolean valueTypeIsStatic, @@ -117,8 +131,18 @@ protected MapSerializer(HashSet ignoredEntries, _property = null; _filterId = null; _sortKeys = false; + _suppressableValue = null; } + /** + * @since 2.5 + */ + protected void _ensureOverride() { + if (getClass() != MapSerializer.class) { + throw new IllegalStateException("Missing override in class "+getClass().getName()); + } + } + @SuppressWarnings("unchecked") protected MapSerializer(MapSerializer src, BeanProperty property, JsonSerializer keySerializer, JsonSerializer valueSerializer, @@ -136,9 +160,19 @@ protected MapSerializer(MapSerializer src, BeanProperty property, _property = property; _filterId = src._filterId; _sortKeys = src._sortKeys; + _suppressableValue = src._suppressableValue; + } + + @Deprecated // since 2.5 + protected MapSerializer(MapSerializer src, TypeSerializer vts) { + this(src, vts, src._suppressableValue); } - protected MapSerializer(MapSerializer src, TypeSerializer vts) + /** + * @since 2.5 + */ + protected MapSerializer(MapSerializer src, TypeSerializer vts, + Object suppressableValue) { super(Map.class, false); _ignoredEntries = src._ignoredEntries; @@ -152,6 +186,7 @@ protected MapSerializer(MapSerializer src, TypeSerializer vts) _property = src._property; _filterId = src._filterId; _sortKeys = src._sortKeys; + _suppressableValue = suppressableValue; } protected MapSerializer(MapSerializer src, Object filterId, boolean sortKeys) @@ -168,11 +203,16 @@ protected MapSerializer(MapSerializer src, Object filterId, boolean sortKeys) _property = src._property; _filterId = filterId; _sortKeys = sortKeys; + _suppressableValue = src._suppressableValue; } - + @Override public MapSerializer _withValueTypeSerializer(TypeSerializer vts) { - return new MapSerializer(this, vts); + if (_valueTypeSerializer == vts) { + return this; + } + _ensureOverride(); + return new MapSerializer(this, vts, null); } /** @@ -182,6 +222,7 @@ public MapSerializer withResolved(BeanProperty property, JsonSerializer keySerializer, JsonSerializer valueSerializer, HashSet ignored, boolean sortKeys) { + _ensureOverride(); MapSerializer ser = new MapSerializer(this, property, keySerializer, valueSerializer, ignored); if (sortKeys != ser._sortKeys) { ser = new MapSerializer(ser, _filterId, sortKeys); @@ -193,8 +234,26 @@ public MapSerializer withResolved(BeanProperty property, * @since 2.3 */ public MapSerializer withFilterId(Object filterId) { - return (_filterId == filterId) ? this : new MapSerializer(this, filterId, _sortKeys); + if (_filterId == filterId) { + return this; + } + _ensureOverride(); + return new MapSerializer(this, filterId, _sortKeys); } + + /** + * Mutant factory for constructing an instance with different inclusion strategy + * for content (Map values). + * + * @since 2.5 + */ + public MapSerializer withContentInclusion(Object suppressableValue) { + if (suppressableValue == _suppressableValue) { + return this; + } + _ensureOverride(); + return new MapSerializer(this, _valueTypeSerializer, suppressableValue); + } /** * @since 2.3 @@ -204,7 +263,9 @@ public static MapSerializer construct(String[] ignoredList, JavaType mapType, JsonSerializer keySerializer, JsonSerializer valueSerializer, Object filterId) { - HashSet ignoredEntries = toSet(ignoredList); + HashSet ignoredEntries = (ignoredList == null || ignoredList.length == 0) + ? null : ArrayBuilders.arrayToSet(ignoredList); + JavaType keyType, valueType; if (mapType == null) { @@ -230,17 +291,6 @@ public static MapSerializer construct(String[] ignoredList, JavaType mapType, return ser; } - private static HashSet toSet(String[] ignoredEntries) { - if (ignoredEntries == null || ignoredEntries.length == 0) { - return null; - } - HashSet result = new HashSet(ignoredEntries.length); - for (String prop : ignoredEntries) { - result.add(prop); - } - return result; - } - /* /********************************************************** /* Post-processing (contextualization) @@ -260,6 +310,7 @@ public JsonSerializer createContextual(SerializerProvider provider, JsonSerializer keySer = null; final AnnotationIntrospector intr = provider.getAnnotationIntrospector(); final AnnotatedMember propertyAcc = (property == null) ? null : property.getMember(); + Object suppressableValue = _suppressableValue; // First: if we have a property, may have property-annotation overrides if (propertyAcc != null && intr != null) { @@ -271,6 +322,10 @@ public JsonSerializer createContextual(SerializerProvider provider, if (serDef != null) { ser = provider.serializerInstance(propertyAcc, serDef); } + JsonInclude.Include incl = intr.findSerializationInclusionForContent(propertyAcc, null); + if (incl != null) { + suppressableValue = incl; + } } if (ser == null) { ser = _valueSerializer; @@ -310,6 +365,9 @@ public JsonSerializer createContextual(SerializerProvider provider, sortKeys = (b != null) && b.booleanValue(); } MapSerializer mser = withResolved(property, keySer, ser, ignored, sortKeys); + if (suppressableValue != _suppressableValue) { + mser = mser.withContentInclusion(suppressableValue); + } // [Issue#307]: allow filtering if (property != null) { @@ -379,16 +437,27 @@ public void serialize(Map value, JsonGenerator jgen, SerializerProvider pro { jgen.writeStartObject(); if (!value.isEmpty()) { + Object suppressableValue = _suppressableValue; + if (suppressableValue == null) { + if (!provider.isEnabled(SerializationFeature.WRITE_NULL_MAP_VALUES)) { + suppressableValue = JsonInclude.Include.NON_NULL; + } + + } if (_filterId != null) { serializeFilteredFields(value, jgen, provider, - findPropertyFilter(provider, _filterId, value)); + findPropertyFilter(provider, _filterId, value), suppressableValue); jgen.writeEndObject(); return; } if (_sortKeys || provider.isEnabled(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS)) { value = _orderEntries(value); } - if (_valueSerializer != null) { + if (suppressableValue != null) { +// !!! TEST + serializeFields(value, jgen, provider); +// serializeOptionalFields(value, jgen, provider); + } else if (_valueSerializer != null) { serializeFieldsUsing(value, jgen, provider, _valueSerializer); } else { serializeFields(value, jgen, provider); @@ -404,10 +473,20 @@ public void serializeWithType(Map value, JsonGenerator jgen, SerializerProv { typeSer.writeTypePrefixForObject(value, jgen); if (!value.isEmpty()) { + Object suppressableValue = _suppressableValue; + if (suppressableValue == null) { + if (!provider.isEnabled(SerializationFeature.WRITE_NULL_MAP_VALUES)) { + suppressableValue = JsonInclude.Include.NON_NULL; + } + + } if (_sortKeys || provider.isEnabled(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS)) { value = _orderEntries(value); } - if (_valueSerializer != null) { + if (suppressableValue != null) { +// !!! TEST + serializeFields(value, jgen, provider); + } else if (_valueSerializer != null) { serializeFieldsUsing(value, jgen, provider, _valueSerializer); } else { serializeFields(value, jgen, provider); @@ -532,7 +611,8 @@ protected void serializeFieldsUsing(Map value, JsonGenerator jgen, Serializ * @since 2.3 */ public void serializeFilteredFields(Map value, JsonGenerator jgen, SerializerProvider provider, - PropertyFilter filter) + PropertyFilter filter, + Object suppressableValue) // since 2.5 throws IOException { final HashSet ignored = _ignoredEntries; @@ -582,8 +662,15 @@ public void serializeFilteredFields(Map value, JsonGenerator jgen, Serializ } } } + + @Deprecated // since 2.5 + public void serializeFilteredFields(Map value, JsonGenerator gen, SerializerProvider provider, + PropertyFilter filter) throws IOException + { + serializeFilteredFields(value, gen, provider, filter, null); + } - protected void serializeTypedFields(Map value, JsonGenerator jgen, SerializerProvider provider) + protected void serializeTypedFields(Map value, JsonGenerator gen, SerializerProvider provider) throws IOException { final JsonSerializer keySerializer = _keySerializer; @@ -597,18 +684,18 @@ protected void serializeTypedFields(Map value, JsonGenerator jgen, Serializ // First, serialize key Object keyElem = entry.getKey(); if (keyElem == null) { - provider.findNullKeySerializer(_keyType, _property).serialize(null, jgen, provider); + provider.findNullKeySerializer(_keyType, _property).serialize(null, gen, provider); } else { // [JACKSON-314] also may need to skip entries with null values if (skipNulls && valueElem == null) continue; // One twist: is entry ignorable? If so, skip if (ignored != null && ignored.contains(keyElem)) continue; - keySerializer.serialize(keyElem, jgen, provider); + keySerializer.serialize(keyElem, gen, provider); } // And then value if (valueElem == null) { - provider.defaultSerializeNull(jgen); + provider.defaultSerializeNull(gen); } else { Class cc = valueElem.getClass(); JsonSerializer currSerializer; @@ -624,7 +711,7 @@ protected void serializeTypedFields(Map value, JsonGenerator jgen, Serializ prevValueClass = cc; } try { - currSerializer.serializeWithType(valueElem, jgen, provider, _valueTypeSerializer); + currSerializer.serializeWithType(valueElem, gen, provider, _valueTypeSerializer); } catch (Exception e) { // [JACKSON-55] Need to add reference information String keyDesc = ""+keyElem; @@ -633,6 +720,12 @@ protected void serializeTypedFields(Map value, JsonGenerator jgen, Serializ } } } + + /* + /********************************************************** + /* Schema related functionality + /********************************************************** + */ @Override public JsonNode getSchema(SerializerProvider provider, Type typeHint) diff --git a/src/test/java/com/fasterxml/jackson/databind/ser/TestMapSerialization.java b/src/test/java/com/fasterxml/jackson/databind/ser/TestMapSerialization.java index 7d36e09ed2..8962c787fd 100644 --- a/src/test/java/com/fasterxml/jackson/databind/ser/TestMapSerialization.java +++ b/src/test/java/com/fasterxml/jackson/databind/ser/TestMapSerialization.java @@ -99,6 +99,15 @@ public NoNullValuesMapContainer add(String key, String value) { return this; } } + + // [databind#527] + @JsonInclude(content=JsonInclude.Include.NON_NULL) + static class NoNullsStringMap extends LinkedHashMap { + public NoNullsStringMap add(String key, String value) { + put(key, value); + return this; + } + } /* /********************************************************** @@ -201,7 +210,18 @@ public void testEnumMapEntry() throws IOException assertEquals(aposToQuotes("[{'answer':42}]"), json); } + // [databind#527] public void testNonNullValueMap() throws IOException + { + String json = MAPPER.writeValueAsString(new NoNullsStringMap() + .add("a", "foo") + .add("b", null) + .add("c", "bar")); + assertEquals(aposToQuotes("{'stuff':{'a':'foo','c':'bar'}}"), json); + } + + // [databind#527] + public void testNonNullValueMapViaProp() throws IOException { String json = MAPPER.writeValueAsString(new NoNullValuesMapContainer() .add("a", "foo")