diff --git a/release-notes/CREDITS b/release-notes/CREDITS index 39a8b6e40d..fbc3c08dac 100644 --- a/release-notes/CREDITS +++ b/release-notes/CREDITS @@ -530,6 +530,10 @@ Andrew Snare (asnare@github) * Reported #1315: Binding numeric values can BigDecimal lose precision (2.8.2) +Gili Tzabari (cowwoc@github) + * Reported #1351: `@JsonInclude(NON_DEFAULT)` doesn't omit null fields + (2.8.3) + Connor Kuhn (ckuhn@github) * Contributed #1341: FAIL_ON_MISSING_EXTERNAL_TYPE_ID_PROPERTY (2.9.0) diff --git a/release-notes/VERSION b/release-notes/VERSION index 837d59db07..096eb635e3 100644 --- a/release-notes/VERSION +++ b/release-notes/VERSION @@ -11,6 +11,8 @@ Project: jackson-databind 2.8.3 (not yet released) +#1351: `@JsonInclude(NON_DEFAULT)` doesn't omit null fields + (reported by Gili T) #1353: Improve error-handling for `java.net.URL` deserialization #1361: Change `TokenBuffer` to use new `writeEmbeddedObject()` if possible diff --git a/src/main/java/com/fasterxml/jackson/databind/BeanDescription.java b/src/main/java/com/fasterxml/jackson/databind/BeanDescription.java index c11fbea072..2a4257e576 100644 --- a/src/main/java/com/fasterxml/jackson/databind/BeanDescription.java +++ b/src/main/java/com/fasterxml/jackson/databind/BeanDescription.java @@ -195,6 +195,12 @@ protected BeanDescription(JavaType type) { */ /** + * Method for finding annotation-indicated inclusion definition (if any); + * possibly overriding given default value. + *

+ * NOTE: does NOT use global inclusion default settings as the base, unless + * passed as `defValue`. + * * @since 2.7 */ public abstract JsonInclude.Value findPropertyInclusion(JsonInclude.Value defValue); 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 37ad2a7aca..5f16ff55cb 100644 --- a/src/main/java/com/fasterxml/jackson/databind/introspect/BasicBeanDescription.java +++ b/src/main/java/com/fasterxml/jackson/databind/introspect/BasicBeanDescription.java @@ -391,8 +391,7 @@ public JsonInclude.Value findPropertyInclusion(JsonInclude.Value defValue) { if (_annotationIntrospector != null) { JsonInclude.Value incl = _annotationIntrospector.findPropertyInclusion(_classInfo); if (incl != null) { - return (defValue == null) ? incl - : defValue.withOverrides(incl); + return (defValue == null) ? incl : defValue.withOverrides(incl); } } return defValue; 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 c82edf5e3e..138f7930f5 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/PropertyBuilder.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/PropertyBuilder.java @@ -16,17 +16,10 @@ public class PropertyBuilder { // @since 2.7 private final static Object NO_DEFAULT_MARKER = Boolean.FALSE; - + final protected SerializationConfig _config; final protected BeanDescription _beanDesc; - /** - * Default inclusion mode for properties of the POJO for which - * properties are collected; possibly overridden on - * per-property basis. - */ - final protected JsonInclude.Value _defaultInclusion; - final protected AnnotationIntrospector _annotationIntrospector; /** @@ -40,14 +33,41 @@ public class PropertyBuilder */ protected Object _defaultBean; + /** + * Default inclusion mode for properties of the POJO for which + * properties are collected; possibly overridden on + * per-property basis. Combines global inclusion defaults and + * per-type (annotation and type-override) inclusion overrides. + */ + final protected JsonInclude.Value _defaultInclusion; + + /** + * Marker flag used to indicate that "real" default values are to be used + * for properties, as per per-type value inclusion of type NON_DEFAULT + * + * @since 2.8 + */ + final protected boolean _useRealPropertyDefaults; + public PropertyBuilder(SerializationConfig config, BeanDescription beanDesc) { _config = config; _beanDesc = beanDesc; - // NOTE: this includes global defaults and defaults of POJO that contains property, - // but not defaults for types of properties referenced - _defaultInclusion = beanDesc.findPropertyInclusion( - config.getDefaultPropertyInclusion(beanDesc.getBeanClass())); + // 08-Sep-2016, tatu: This gets tricky, with 3 levels of definitions: + // (a) global default inclusion + // (b) per-type default inclusion (from annotation or config overrides; + // latter having precedence + // Cc) per-property override + // + // and not only requiring merging, but also considering special handling + // for NON_DEFAULT in case of (b) (vs (a) or (c)) + JsonInclude.Value inclPerType = JsonInclude.Value.merge( + beanDesc.findPropertyInclusion(JsonInclude.Value.empty()), + config.getDefaultPropertyInclusion(beanDesc.getBeanClass(), + JsonInclude.Value.empty())); + _defaultInclusion = JsonInclude.Value.merge(config.getDefaultPropertyInclusion(), + inclPerType); + _useRealPropertyDefaults = inclPerType.getValueInclusion() == JsonInclude.Include.NON_DEFAULT; _annotationIntrospector = _config.getAnnotationIntrospector(); } @@ -123,8 +143,8 @@ protected BeanPropertyWriter buildWriter(SerializerProvider prov, // so that if enclosing class has this, we may need to access values of property, // whereas for global defaults OR per-property overrides, we have more // static definition. Sigh. - // First: case of class specifying it; try to find POJO property defaults - if (_defaultInclusion.getValueInclusion() == JsonInclude.Include.NON_DEFAULT) { + // First: case of class/type specifying it; try to find POJO property defaults + if (_useRealPropertyDefaults) { // 07-Sep-2016, tatu: may also need to front-load access forcing now if (prov.isEnabled(MapperFeature.CAN_OVERRIDE_ACCESS_MODIFIERS)) { am.fixAccess(_config.isEnabled(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS)); @@ -132,6 +152,7 @@ protected BeanPropertyWriter buildWriter(SerializerProvider prov, valueToSuppress = getPropertyDefaultValue(propDef.getName(), am, actualType); } else { valueToSuppress = getDefaultValue(actualType); + suppressNulls = true; } if (valueToSuppress == null) { suppressNulls = true; @@ -140,7 +161,6 @@ protected BeanPropertyWriter buildWriter(SerializerProvider prov, valueToSuppress = ArrayBuilders.getArrayComparator(valueToSuppress); } } - break; case NON_ABSENT: // new with 2.6, to support Guava/JDK8 Optionals // always suppress nulls diff --git a/src/test/java/com/fasterxml/jackson/databind/filter/JsonIncludeTest.java b/src/test/java/com/fasterxml/jackson/databind/filter/JsonIncludeTest.java index 851b1103af..2e51cfd466 100644 --- a/src/test/java/com/fasterxml/jackson/databind/filter/JsonIncludeTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/filter/JsonIncludeTest.java @@ -261,4 +261,23 @@ public void testPropConfigOverridesForInclude() throws IOException assertEquals(aposToQuotes("{'map':{}}"), mapper.writeValueAsString(empty)); } + + static class Issue1351Bean + { + public final String first; + public final double second; + + public Issue1351Bean(String first, double second) { + this.first = first; + this.second = second; + } + } + + public void testIssue1351() throws Exception + { + ObjectMapper mapper = new ObjectMapper(); + mapper.setSerializationInclusion(JsonInclude.Include.NON_DEFAULT); + assertEquals(aposToQuotes("{}"), + mapper.writeValueAsString(new Issue1351Bean(null, (double) 0))); + } }