diff options
author | Tatu Saloranta <tatu.saloranta@iki.fi> | 2016-09-30 17:26:12 -0700 |
---|---|---|
committer | Tatu Saloranta <tatu.saloranta@iki.fi> | 2016-09-30 17:26:12 -0700 |
commit | cdf8041a29df98179f47f51747df1ce930c0e6c3 (patch) | |
tree | b26a9e6c7ca620b985303b1bf0d466753aa645c1 | |
parent | 0c4e156cea2a4823bff884d1a4a3010a127ff96f (diff) | |
download | jackson-databind-cdf8041a29df98179f47f51747df1ce930c0e6c3.tar.gz |
More work on #888; rewriting Map-include handling, adding temporarily test failures
8 files changed, 424 insertions, 197 deletions
diff --git a/src/main/java/com/fasterxml/jackson/databind/SerializerProvider.java b/src/main/java/com/fasterxml/jackson/databind/SerializerProvider.java index e8d6cd4db..698aeab24 100644 --- a/src/main/java/com/fasterxml/jackson/databind/SerializerProvider.java +++ b/src/main/java/com/fasterxml/jackson/databind/SerializerProvider.java @@ -930,12 +930,24 @@ public abstract class SerializerProvider * filter instance, * given a {@link Class} to instantiate (with default constructor, by default). * + * @param forProperty (optional) If filter is created for a property, that property; + * `null` if filter created via defaulting, global or per-type. + * * @since 2.9 */ public abstract Object includeFilterInstance(BeanPropertyDefinition forProperty, Class<?> filterClass) throws JsonMappingException; + /** + * Follow-up method that may be called after calling {@link #includeFilterInstance}, + * to check handling of `null` values by the filter. + * + * @since 2.9 + */ + public abstract boolean includeFilterSuppressNulls(Object filter) + throws JsonMappingException; + /* /********************************************************** /* Support for contextualization @@ -1180,7 +1192,17 @@ public abstract class SerializerProvider e.initCause(cause); throw e; } - + + /** + * @since 2.9 + */ + public <T> T reportBadDefinition(Class<?> raw, String msg, Throwable cause) + throws JsonMappingException { + InvalidDefinitionException e = InvalidDefinitionException.from(getGenerator(), msg, constructType(raw)); + e.initCause(cause); + throw e; + } + /** * Helper method called to indicate problem; default behavior is to construct and * throw a {@link JsonMappingException}, but in future may collect more than one 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 29c4a4900..b44f4dd18 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/BasicSerializerFactory.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/BasicSerializerFactory.java @@ -774,12 +774,7 @@ public abstract class BasicSerializerFactory MapSerializer mapSer = MapSerializer.construct(ignored, type, staticTyping, elementTypeSerializer, keySerializer, elementValueSerializer, filterId); - Object suppressableValue = findSuppressableContentValue(config, - type.getContentType(), beanDesc); - if (suppressableValue != null) { - mapSer = mapSer.withContentInclusion(suppressableValue); - } - ser = mapSer; + ser = _checkMapContentInclusion(prov, beanDesc, mapSer); } } // [databind#120]: Allow post-processing @@ -792,42 +787,95 @@ public abstract class BasicSerializerFactory } /** - *<p> - * NOTE: although return type is left opaque, it really needs to be - * <code>JsonInclude.Include</code> for things to work as expected. + * Helper method that does figures out content inclusion value to use, if any, + * and construct re-configured {@link MapSerializer} appropriately. * - * @since 2.5 + * @since 2.9 */ - protected Object findSuppressableContentValue(SerializationConfig config, - JavaType contentType, BeanDescription beanDesc) + protected MapSerializer _checkMapContentInclusion(SerializerProvider prov, + BeanDescription beanDesc, MapSerializer mapSer) throws JsonMappingException { - /* 16-Apr-2016, tatu: Should this consider possible property-config overrides? - * Quite possibly yes, but would need to carefully check that content type being - * used is appropriate. - */ - JsonInclude.Value inclV = beanDesc.findPropertyInclusion(config.getDefaultPropertyInclusion()); + final SerializationConfig config = prov.getConfig(); - if (inclV == null) { - return null; + // 30-Sep-2016, tatu: Defaulting gets complicated because we might have two distinct + // axis to consider: Map itself, and then value type. + // Start with Map-defaults, then use more-specific value override, if any + + // Start by getting global setting, overridden by Map-type-override + JsonInclude.Value inclV = config.getDefaultPropertyInclusion(Map.class, + config.getDefaultPropertyInclusion()); + // and then merge content-type overrides, if any. But note that there's + // content-to-value inclusion shift we have to do + final JavaType contentType = mapSer.getContentType(); + JsonInclude.Value valueIncl = config.getDefaultPropertyInclusion(contentType.getRawClass(), null); + + if (valueIncl != null) { + switch (valueIncl.getValueInclusion()) { + case USE_DEFAULTS: + break; + case CUSTOM: + inclV = inclV.withContentFilter(valueIncl.getContentFilter()); + break; + default: + inclV = inclV.withContentInclusion(valueIncl.getValueInclusion()); + } } - JsonInclude.Include incl = inclV.getContentInclusion(); - switch (incl) { - case USE_DEFAULTS: // means "dunno" - return null; + JsonInclude.Include incl = (inclV == null) ? JsonInclude.Include.USE_DEFAULTS : inclV.getContentInclusion(); + if (incl == JsonInclude.Include.USE_DEFAULTS) { + if (!config.isEnabled(SerializationFeature.WRITE_NULL_MAP_VALUES)) { + return mapSer.withContentInclusion(null, true); + } + return mapSer; + } + // NOTE: mostly copied from `PropertyBuilder`; would be nice to refactor + // but code is not identical nor are these types related + Object valueToSuppress; + boolean suppressNulls; + + switch (inclV.getContentInclusion()) { case NON_DEFAULT: - // 19-Oct-2014, tatu: Not sure what this'd mean; so take it to mean "NON_EMPTY"... - // 11-Nov-2015, tatu: With 2.6, we did indeed revert to "NON_EMPTY", but that did - // not go well, so with 2.7, we'll do this instead... - // But not 100% sure if we ought to call new `JsonSerializer.findDefaultValue()`; - // to do that, would need to locate said serializer -// incl = JsonInclude.Include.NON_EMPTY; + valueToSuppress = BeanUtil.getDefaultValue(contentType); + suppressNulls = true; + if (valueToSuppress != null) { + if (valueToSuppress.getClass().isArray()) { + valueToSuppress = ArrayBuilders.getArrayComparator(valueToSuppress); + } + } break; + case NON_ABSENT: // new with 2.6, to support Guava/JDK8 Optionals + // always suppress nulls + suppressNulls = true; + // and for referential types, also "empty", which in their case means "absent" + valueToSuppress = contentType.isReferenceType() + ? MapSerializer.MARKER_FOR_EMPTY : null; + break; + case NON_EMPTY: + // always suppress nulls + suppressNulls = true; + // but possibly also 'empty' values: + valueToSuppress = MapSerializer.MARKER_FOR_EMPTY; + break; + case CUSTOM: // new with 2.9 + valueToSuppress = prov.includeFilterInstance(null, inclV.getContentFilter()); + if (valueToSuppress == null) { // is this legal? + suppressNulls = true; + } else { + suppressNulls = prov.includeFilterSuppressNulls(valueToSuppress); + } + break; + case NON_NULL: + valueToSuppress = null; + suppressNulls = true; + // fall through + case ALWAYS: // default default: - // all other modes actually good as is, unless we'll find better ways + valueToSuppress = null; + suppressNulls = !prov.isEnabled(SerializationFeature.WRITE_NULL_MAP_VALUES); break; } - return incl; + + return mapSer.withContentInclusion(valueToSuppress, suppressNulls); } /* diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/DefaultSerializerProvider.java b/src/main/java/com/fasterxml/jackson/databind/ser/DefaultSerializerProvider.java index d89cb052c..118a82b3a 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/DefaultSerializerProvider.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/DefaultSerializerProvider.java @@ -155,6 +155,25 @@ public abstract class DefaultSerializerProvider return filter; } + @Override + public boolean includeFilterSuppressNulls(Object filter) throws JsonMappingException + { + if (filter == null) { + return true; + } + // should let filter decide what to do with nulls: + // But just case, let's handle unexpected (from our perspective) problems explicitly + try { + return filter.equals(null); + } catch (Throwable t) { + String msg = String.format( +"Problem determining whether filter of type '%s' should filter out `null` values: (%s) %s", +filter.getClass().getName(), t.getClass().getName(), t.getMessage()); + reportBadDefinition(filter.getClass(), msg, t); + return false; // never gets here + } + } + /* /********************************************************** /* Object Id handling diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/PropertyBuilder.java b/src/main/java/com/fasterxml/jackson/databind/ser/PropertyBuilder.java index 95997509c..7638438db 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/PropertyBuilder.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/PropertyBuilder.java @@ -143,7 +143,6 @@ public class PropertyBuilder if (inclusion == JsonInclude.Include.USE_DEFAULTS) { // should not occur but... inclusion = JsonInclude.Include.ALWAYS; } - switch (inclusion) { case NON_DEFAULT: // 11-Nov-2015, tatu: This is tricky because semantics differ between cases, @@ -158,7 +157,7 @@ public class PropertyBuilder } valueToSuppress = getPropertyDefaultValue(propDef.getName(), am, actualType); } else { - valueToSuppress = getDefaultValue(actualType); + valueToSuppress = BeanUtil.getDefaultValue(actualType); suppressNulls = true; } if (valueToSuppress == null) { @@ -185,20 +184,10 @@ public class PropertyBuilder break; case CUSTOM: // new with 2.9 valueToSuppress = prov.includeFilterInstance(propDef, inclV.getValueFilter()); - if (valueToSuppress == null) { // is this legal? suppressNulls = true; } else { - // should let filter decide what to do with nulls: - // But just case, let's handle unexpected (from our perspective) problems explicitly - try { - suppressNulls = valueToSuppress.equals(null); - } catch (Throwable t) { - String msg = String.format( -"Problem determining whether filter of type '%s' should filter out `null` values: (%s) %s", -valueToSuppress.getClass().getName(), t.getClass().getName(), t.getMessage()); - prov.reportBadDefinition(_beanDesc.getType(), msg, t); - } + suppressNulls = prov.includeFilterSuppressNulls(valueToSuppress); } break; case NON_NULL: @@ -342,37 +331,13 @@ valueToSuppress.getClass().getName(), t.getClass().getName(), t.getMessage()); } /** - * Accessor used to find out "default value" to use for comparing values to - * serialize, to determine whether to exclude value from serialization with - * inclusion type of {@link com.fasterxml.jackson.annotation.JsonInclude.Include#NON_DEFAULT}. - *<p> - * Default logic is such that for primitives and wrapper types for primitives, expected - * defaults (0 for `int` and `java.lang.Integer`) are returned; for Strings, empty String, - * and for structured (Maps, Collections, arrays) and reference types, criteria - * {@link com.fasterxml.jackson.annotation.JsonInclude.Include#NON_DEFAULT} - * is used. - * - * @since 2.7 + * @deprecated Since 2.9 */ - protected Object getDefaultValue(JavaType type) - { - // 06-Nov-2015, tatu: Returning null is fine for Object types; but need special - // handling for primitives since they are never passed as nulls. - Class<?> cls = type.getRawClass(); - - Class<?> prim = ClassUtil.primitiveType(cls); - if (prim != null) { - return ClassUtil.defaultValue(prim); - } - if (type.isContainerType() || type.isReferenceType()) { - return JsonInclude.Include.NON_EMPTY; - } - if (cls == String.class) { - return ""; - } - return null; + @Deprecated // since 2.9 + protected Object getDefaultValue(JavaType type) { + return BeanUtil.getDefaultValue(type); } - + /* /********************************************************** /* Helper methods for exception handling diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/std/BeanSerializerBase.java b/src/main/java/com/fasterxml/jackson/databind/ser/std/BeanSerializerBase.java index b1a36a514..389c2e0ef 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/std/BeanSerializerBase.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/std/BeanSerializerBase.java @@ -494,7 +494,6 @@ public abstract class BeanSerializerBase objectIdInfo.getAlwaysAsId()); } } - // Or change Filter Id in use? Object filterId = intr.findFilterId(accessor); if (filterId != null) { @@ -523,6 +522,7 @@ public abstract class BeanSerializerBase if (shape == null) { shape = _serializationShape; } + // last but not least; may need to transmute into as-array serialization if (shape == JsonFormat.Shape.ARRAY) { return contextual.asArraySerializer(); } 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 9915fb133..3da9858b5 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 @@ -20,6 +20,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; +import com.fasterxml.jackson.databind.util.BeanUtil; /** * Standard serializer implementation for serializing {link java.util.Map} types. @@ -37,14 +38,20 @@ public class MapSerializer protected final static JavaType UNSPECIFIED_TYPE = TypeFactory.unknownType(); /** - * Map-valued property being serialized with this instance + * @since 2.9 */ - protected final BeanProperty _property; + public final static Object MARKER_FOR_EMPTY = JsonInclude.Include.NON_EMPTY; + /* + /********************************************************** + /* Basic information about referring property, type + /********************************************************** + */ + /** - * Set of entries to omit during serialization, if any + * Map-valued property being serialized with this instance */ - protected final Set<String> _ignoredEntries; + protected final BeanProperty _property; /** * Whether static types should be used for serialization of values @@ -62,6 +69,12 @@ public class MapSerializer */ protected final JavaType _valueType; + /* + /********************************************************** + /* Serializers used + /********************************************************** + */ + /** * Key serializer to use, if it can be statically determined */ @@ -83,6 +96,17 @@ public class MapSerializer */ protected PropertySerializerMap _dynamicValueSerializers; + /* + /********************************************************** + /* Config settings, filtering + /********************************************************** + */ + + /** + * Set of entries to omit during serialization, if any + */ + protected final Set<String> _ignoredEntries; + /** * Id of the property filter to use, if any; null if none. * @@ -91,14 +115,6 @@ public class MapSerializer protected final Object _filterId; /** - * Flag set if output is forced to be sorted by keys (usually due - * to annotation). - * - * @since 2.4 - */ - protected final boolean _sortKeys; - - /** * Value that indicates suppression mechanism to use for <b>values contained</b>; * either one of values of {@link com.fasterxml.jackson.annotation.JsonInclude.Include}, * or actual object to compare against ("default value"). @@ -109,6 +125,28 @@ public class MapSerializer */ protected final Object _suppressableValue; + /** + * Flag that indicates what to do with `null` values, distinct from + * handling of {@link #_suppressableValue} + * + * @since 2.9 + */ + protected final boolean _suppressNulls; + + /* + /********************************************************** + /* Config settings, other + /********************************************************** + */ + + /** + * Flag set if output is forced to be sorted by keys (usually due + * to annotation). + * + * @since 2.4 + */ + protected final boolean _sortKeys; + /* /********************************************************** /* Life-cycle @@ -138,17 +176,9 @@ public class MapSerializer _filterId = null; _sortKeys = false; _suppressableValue = null; + _suppressNulls = false; } - /** - * @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, @@ -168,18 +198,14 @@ public class MapSerializer _filterId = src._filterId; _sortKeys = src._sortKeys; _suppressableValue = src._suppressableValue; - } - - @Deprecated // since 2.5 - protected MapSerializer(MapSerializer src, TypeSerializer vts) { - this(src, vts, src._suppressableValue); + _suppressNulls = src._suppressNulls; } /** - * @since 2.5 + * @since 2.9 */ protected MapSerializer(MapSerializer src, TypeSerializer vts, - Object suppressableValue) + Object suppressableValue, boolean suppressNulls) { super(Map.class, false); _ignoredEntries = src._ignoredEntries; @@ -193,12 +219,8 @@ public class MapSerializer _property = src._property; _filterId = src._filterId; _sortKeys = src._sortKeys; - // 05-Jun-2015, tatu: For referential, this is same as NON_EMPTY; for others, NON_NULL, so: - if (suppressableValue == JsonInclude.Include.NON_ABSENT) { - suppressableValue = _valueType.isReferenceType() ? - JsonInclude.Include.NON_EMPTY : JsonInclude.Include.NON_NULL; - } _suppressableValue = suppressableValue; + _suppressNulls = suppressNulls; } protected MapSerializer(MapSerializer src, Object filterId, boolean sortKeys) @@ -216,6 +238,7 @@ public class MapSerializer _filterId = filterId; _sortKeys = sortKeys; _suppressableValue = src._suppressableValue; + _suppressNulls = src._suppressNulls; } @Override @@ -224,7 +247,7 @@ public class MapSerializer return this; } _ensureOverride(); - return new MapSerializer(this, vts, null); + return new MapSerializer(this, vts, _suppressableValue, _suppressNulls); } /** @@ -255,32 +278,15 @@ public class MapSerializer * Mutant factory for constructing an instance with different inclusion strategy * for content (Map values). * - * @since 2.5 + * @since 2.9 */ - public MapSerializer withContentInclusion(Object suppressableValue) { - if (suppressableValue == _suppressableValue) { + public MapSerializer withContentInclusion(Object suppressableValue, boolean suppressNulls) { + if ((suppressableValue == _suppressableValue) && (_suppressNulls == suppressNulls)) { return this; } _ensureOverride(); - return new MapSerializer(this, _valueTypeSerializer, suppressableValue); + return new MapSerializer(this, _valueTypeSerializer, suppressableValue, suppressNulls); } - - /** - * @since 2.3 - * - * @deprecated Since 2.8 use the other overload - */ - @Deprecated // since 2.8 - public static MapSerializer construct(String[] ignoredList, JavaType mapType, - boolean staticValueType, TypeSerializer vts, - JsonSerializer<Object> keySerializer, JsonSerializer<Object> valueSerializer, - Object filterId) - { - Set<String> ignoredEntries = (ignoredList == null || ignoredList.length == 0) - ? null : ArrayBuilders.arrayToSet(ignoredList); - return construct(ignoredEntries, mapType, staticValueType, vts, - keySerializer, valueSerializer, filterId); - } /** * @since 2.8 @@ -315,6 +321,61 @@ public class MapSerializer return ser; } + /** + * @since 2.5 + */ + protected void _ensureOverride() { + if (getClass() != MapSerializer.class) { + throw new IllegalStateException("Missing override in class "+getClass().getName()); + } + } + + /* + /********************************************************** + /* Deprecated creators + /********************************************************** + */ + + /** + * @since 2.5 + * @deprecated // since 2.9 + */ + @Deprecated // since 2.9 + protected MapSerializer(MapSerializer src, TypeSerializer vts, + Object suppressableValue) + { + this(src, vts, suppressableValue, false); + } + + /** + * @deprecated since 2.9 + */ + @Deprecated // since 2.9 + public MapSerializer withContentInclusion(Object suppressableValue) { + if (suppressableValue == _suppressableValue) { + return this; + } + _ensureOverride(); + return new MapSerializer(this, _valueTypeSerializer, suppressableValue, _suppressNulls); + } + + /** + * @since 2.3 + * + * @deprecated Since 2.8 use the other overload + */ + @Deprecated // since 2.8 + public static MapSerializer construct(String[] ignoredList, JavaType mapType, + boolean staticValueType, TypeSerializer vts, + JsonSerializer<Object> keySerializer, JsonSerializer<Object> valueSerializer, + Object filterId) + { + Set<String> ignoredEntries = (ignoredList == null || ignoredList.length == 0) + ? null : ArrayBuilders.arrayToSet(ignoredList); + return construct(ignoredEntries, mapType, staticValueType, vts, + keySerializer, valueSerializer, filterId); + } + /* /********************************************************** /* Post-processing (contextualization) @@ -330,7 +391,6 @@ public class MapSerializer 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)) { @@ -343,12 +403,6 @@ public class MapSerializer ser = provider.serializerInstance(propertyAcc, serDef); } } - - JsonInclude.Value inclV = findIncludeOverrides(provider, property, Map.class); - JsonInclude.Include incl = inclV.getContentInclusion(); - if ((incl != null) && (incl != JsonInclude.Include.USE_DEFAULTS)) { - suppressableValue = incl; - } if (ser == null) { ser = _valueSerializer; } @@ -396,9 +450,6 @@ public class MapSerializer } } MapSerializer mser = withResolved(property, keySer, ser, ignored, sortKeys); - if (suppressableValue != _suppressableValue) { - mser = mser.withContentInclusion(suppressableValue); - } // [databind#307]: allow filtering if (property != null) { @@ -409,10 +460,58 @@ public class MapSerializer mser = mser.withFilterId(filterId); } } + JsonInclude.Value inclV = property.findPropertyInclusion(provider.getConfig(), null); + if (inclV != null) { + JsonInclude.Include incl = inclV.getContentInclusion(); + + if (incl != JsonInclude.Include.USE_DEFAULTS) { + Object valueToSuppress; + boolean suppressNulls; + switch (incl) { + case NON_DEFAULT: + valueToSuppress = BeanUtil.getDefaultValue(_valueType); + suppressNulls = true; + if (valueToSuppress != null) { + if (valueToSuppress.getClass().isArray()) { + valueToSuppress = ArrayBuilders.getArrayComparator(valueToSuppress); + } + } + break; + case NON_ABSENT: + suppressNulls = true; + valueToSuppress = _valueType.isReferenceType() ? MARKER_FOR_EMPTY : null; + break; + case NON_EMPTY: + suppressNulls = true; + valueToSuppress = MapSerializer.MARKER_FOR_EMPTY; + break; + case CUSTOM: + valueToSuppress = provider.includeFilterInstance(null, inclV.getContentFilter()); + if (valueToSuppress == null) { // is this legal? + suppressNulls = true; + } else { + suppressNulls = provider.includeFilterSuppressNulls(valueToSuppress); + } + break; + case NON_NULL: + valueToSuppress = null; + suppressNulls = true; + // fall through + case ALWAYS: // default + default: + valueToSuppress = null; + // 30-Sep-2016, tatu: Should not check global flags here, if inclusion forced + // to be ALWAYS + suppressNulls = false; + break; + } + mser = mser.withContentInclusion(valueToSuppress, suppressNulls); + } + } } return mser; } - + /* /********************************************************** /* Accessors @@ -437,16 +536,27 @@ public class MapSerializer } // 05-Nove-2015, tatu: Simple cases are cheap, but for recursive // emptiness checking we actually need to see if values are empty as well. - Object supp = this._suppressableValue; + Object supp = _suppressableValue; - if ((supp == null) || (supp == JsonInclude.Include.ALWAYS)) { + if ((supp == null) && !_suppressNulls) { return false; } JsonSerializer<Object> valueSer = _valueSerializer; + final boolean checkEmpty = (MARKER_FOR_EMPTY == supp); if (valueSer != null) { for (Object elemValue : value.values()) { - if ((elemValue != null) && !valueSer.isEmpty(prov, elemValue)) { - return false; + if (elemValue == null) { + if (!_suppressNulls) { + return false; + } + } else if (checkEmpty) { + if (!valueSer.isEmpty(prov, elemValue)) { + return false; + } + } else if (supp != null) { + if (!supp.equals(value)) { + return false; + } } } return true; @@ -455,6 +565,9 @@ public class MapSerializer PropertySerializerMap serializers = _dynamicValueSerializers; for (Object elemValue : value.values()) { if (elemValue == null) { + if (!_suppressNulls) { + return false; + } continue; } Class<?> cc = elemValue.getClass(); @@ -470,8 +583,14 @@ public class MapSerializer } serializers = _dynamicValueSerializers; } - if (!valueSer.isEmpty(prov, elemValue)) { - return false; + if (checkEmpty) { + if (!valueSer.isEmpty(prov, elemValue)) { + return false; + } + } else if (supp != null) { + if (!supp.equals(value)) { + return false; + } } } return true; @@ -481,7 +600,7 @@ public class MapSerializer public boolean hasSingleElement(Map<?,?> value) { return (value.size() == 1); } - + /* /********************************************************** /* Extended API @@ -514,22 +633,14 @@ public class MapSerializer { gen.writeStartObject(value); if (!value.isEmpty()) { - Object suppressableValue = _suppressableValue; - if (suppressableValue == JsonInclude.Include.ALWAYS) { - suppressableValue = null; - } else 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 (_filterId != null) { serializeFilteredFields(value, gen, provider, - findPropertyFilter(provider, _filterId, value), suppressableValue); - } else if (suppressableValue != null) { - serializeOptionalFields(value, gen, provider, suppressableValue); + findPropertyFilter(provider, _filterId, value), _suppressableValue); + } else if ((_suppressableValue != null) || _suppressNulls) { + serializeOptionalFields(value, gen, provider, _suppressableValue); } else if (_valueSerializer != null) { serializeFieldsUsing(value, gen, provider, _valueSerializer); } else { @@ -548,22 +659,14 @@ public class MapSerializer // [databind#631]: Assign current value, to be accessible by custom serializers gen.setCurrentValue(value); if (!value.isEmpty()) { - Object suppressableValue = _suppressableValue; - if (suppressableValue == JsonInclude.Include.ALWAYS) { - suppressableValue = null; - } else 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 (_filterId != null) { serializeFilteredFields(value, gen, provider, - findPropertyFilter(provider, _filterId, value), suppressableValue); - } else if (suppressableValue != null) { - serializeOptionalFields(value, gen, provider, suppressableValue); + findPropertyFilter(provider, _filterId, value), _suppressableValue); + } else if ((_suppressableValue != null) || _suppressNulls) { + serializeOptionalFields(value, gen, provider, _suppressableValue); } else if (_valueSerializer != null) { serializeFieldsUsing(value, gen, provider, _valueSerializer); } else { @@ -653,6 +756,7 @@ public class MapSerializer } final Set<String> ignored = _ignoredEntries; PropertySerializerMap serializers = _dynamicValueSerializers; + final boolean checkEmpty = (MARKER_FOR_EMPTY == suppressableValue); for (Map.Entry<?,?> entry : value.entrySet()) { // First find key serializer @@ -669,7 +773,7 @@ public class MapSerializer final Object valueElem = entry.getValue(); JsonSerializer<Object> valueSer; if (valueElem == null) { - if (suppressableValue != null) { // all suppressions include null-suppression + if (_suppressNulls) { // all suppressions include null-suppression continue; } valueSer = provider.getDefaultNullValueSerializer(); @@ -689,9 +793,14 @@ public class MapSerializer } } // also may need to skip non-empty values: - if ((suppressableValue == JsonInclude.Include.NON_EMPTY) - && valueSer.isEmpty(provider, valueElem)) { - continue; + if (checkEmpty) { + if (valueSer.isEmpty(provider, valueElem)) { + continue; + } + } else if (suppressableValue != null) { + if (suppressableValue.equals(valueElem)) { + continue; + } } } // and then serialize, if all went well @@ -760,7 +869,8 @@ public class MapSerializer PropertySerializerMap serializers = _dynamicValueSerializers; final MapProperty prop = new MapProperty(_valueTypeSerializer, _property); - + final boolean checkEmpty = (MARKER_FOR_EMPTY == suppressableValue); + for (Map.Entry<?,?> entry : value.entrySet()) { // First, serialize key; unless ignorable by key final Object keyElem = entry.getKey(); @@ -778,7 +888,7 @@ public class MapSerializer JsonSerializer<Object> valueSer; // And then value if (valueElem == null) { - if (suppressableValue != null) { // all suppressions include null-suppression + if (_suppressNulls) { continue; } valueSer = provider.getDefaultNullValueSerializer(); @@ -798,9 +908,14 @@ public class MapSerializer } } // also may need to skip non-empty values: - if ((suppressableValue == JsonInclude.Include.NON_EMPTY) - && valueSer.isEmpty(provider, valueElem)) { - continue; + if (checkEmpty) { + if (valueSer.isEmpty(provider, valueElem)) { + continue; + } + } else if (suppressableValue != null) { + if (suppressableValue.equals(valueElem)) { + continue; + } } } // and with that, ask filter to handle it @@ -814,13 +929,6 @@ public class MapSerializer } } - @Deprecated // since 2.5 - public void serializeFilteredFields(Map<?,?> value, JsonGenerator gen, SerializerProvider provider, - PropertyFilter filter) throws IOException { - serializeFilteredFields(value, gen, provider, filter, - provider.isEnabled(SerializationFeature.WRITE_NULL_MAP_VALUES) ? null : JsonInclude.Include.NON_NULL); - } - /** * @since 2.5 */ @@ -830,6 +938,7 @@ public class MapSerializer { final Set<String> ignored = _ignoredEntries; PropertySerializerMap serializers = _dynamicValueSerializers; + final boolean checkEmpty = (MARKER_FOR_EMPTY == suppressableValue); for (Map.Entry<?,?> entry : value.entrySet()) { Object keyElem = entry.getKey(); @@ -846,7 +955,7 @@ public class MapSerializer // And then value JsonSerializer<Object> valueSer; if (valueElem == null) { - if (suppressableValue != null) { // all suppression include null suppression + if (_suppressNulls) { // all suppression include null suppression continue; } valueSer = provider.getDefaultNullValueSerializer(); @@ -864,9 +973,14 @@ public class MapSerializer serializers = _dynamicValueSerializers; } // also may need to skip non-empty values: - if ((suppressableValue == JsonInclude.Include.NON_EMPTY) - && valueSer.isEmpty(provider, valueElem)) { - continue; + if (checkEmpty) { + if (valueSer.isEmpty(provider, valueElem)) { + continue; + } + } else if (suppressableValue != null) { + if (suppressableValue.equals(valueElem)) { + continue; + } } } keySerializer.serialize(keyElem, gen, provider); @@ -879,13 +993,6 @@ public class MapSerializer } } - @Deprecated // since 2.5 - protected void serializeTypedFields(Map<?,?> value, JsonGenerator gen, SerializerProvider provider) - throws IOException { - serializeTypedFields(value, gen, provider, - provider.isEnabled(SerializationFeature.WRITE_NULL_MAP_VALUES) ? null : JsonInclude.Include.NON_NULL); - } - /* /********************************************************** /* Schema related functionality diff --git a/src/main/java/com/fasterxml/jackson/databind/util/BeanUtil.java b/src/main/java/com/fasterxml/jackson/databind/util/BeanUtil.java index 5fcaa8d6a..9b9fb928b 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/BeanUtil.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/BeanUtil.java @@ -1,5 +1,7 @@ package com.fasterxml.jackson.databind.util; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.introspect.AnnotatedMethod; /** @@ -103,6 +105,46 @@ public class BeanUtil /* /********************************************************** + /* Value defaulting helpers + /********************************************************** + */ + + /** + * Accessor used to find out "default value" to use for comparing values to + * serialize, to determine whether to exclude value from serialization with + * inclusion type of {@link com.fasterxml.jackson.annotation.JsonInclude.Include#NON_DEFAULT}. + *<p> + * Default logic is such that for primitives and wrapper types for primitives, expected + * defaults (0 for `int` and `java.lang.Integer`) are returned; for Strings, empty String, + * and for structured (Maps, Collections, arrays) and reference types, criteria + * {@link com.fasterxml.jackson.annotation.JsonInclude.Include#NON_DEFAULT} + * is used. + * + * @since 2.7 + */ + public static Object getDefaultValue(JavaType type) + { + // 06-Nov-2015, tatu: Returning null is fine for Object types; but need special + // handling for primitives since they are never passed as nulls. + Class<?> cls = type.getRawClass(); + + // 30-Sep-2016, tatu: Also works for Wrappers, so both `Integer.TYPE` and `Integer.class` + // would return `Integer.TYPE` + Class<?> prim = ClassUtil.primitiveType(cls); + if (prim != null) { + return ClassUtil.defaultValue(prim); + } + if (type.isContainerType() || type.isReferenceType()) { + return JsonInclude.Include.NON_EMPTY; + } + if (cls == String.class) { + return ""; + } + return null; + } + + /* + /********************************************************** /* Handling property names, deprecated methods /********************************************************** */ diff --git a/src/test/java/com/fasterxml/jackson/databind/filter/JsonIncludeCustomTest.java b/src/test/java/com/fasterxml/jackson/databind/filter/JsonIncludeCustomTest.java index 31a8f4f0a..21b0433cc 100644 --- a/src/test/java/com/fasterxml/jackson/databind/filter/JsonIncludeCustomTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/filter/JsonIncludeCustomTest.java @@ -1,5 +1,8 @@ package com.fasterxml.jackson.databind.filter; +import java.util.LinkedHashMap; +import java.util.Map; + import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.BaseMapTest; @@ -37,6 +40,17 @@ public class JsonIncludeCustomTest extends BaseMapTest public FooBean(String v) { value = v; } } + static class FooMapBean { + @JsonInclude(content=JsonInclude.Include.CUSTOM, + contentFilter=FooFilter.class) + public Map<String,String> stuff = new LinkedHashMap<String,String>(); + + public FooMapBean add(String key, String value) { + stuff.put(key, value); + return this; + } + } + static class BrokenBean { @JsonInclude(value=JsonInclude.Include.CUSTOM, valueFilter=BrokenFilter.class) @@ -59,6 +73,16 @@ public class JsonIncludeCustomTest extends BaseMapTest assertEquals("{}", MAPPER.writeValueAsString(new FooBean("foo"))); } + public void testCustomFilterWithMap() throws Exception + { + FooMapBean input = new FooMapBean() + .add("a", "1") + .add("b", "foo") + .add("c", "2"); + + assertEquals(aposToQuotes("{'stuff':{'a':'1','c':'2'}}"), MAPPER.writeValueAsString(input)); + } + /* /********************************************************** /* Test methods, fail handling |