diff --git a/release-notes/VERSION b/release-notes/VERSION index 7891430594..08622e7a42 100644 --- a/release-notes/VERSION +++ b/release-notes/VERSION @@ -12,6 +12,7 @@ Project: jackson-databind (reported by Lovro P, lpandzic@github) #521: Keep bundle annotations, prevent problems with recursive annotation types (reported by tea-dragon@github) +#527: Add support for `@JsonInclude(contents=Include.NON_NULL)` (and others) for Maps #528: Add support for `JsonType.As.EXISTING_PROPERTY` (reported by heapifyman@github; implemented by fleebytes@github) #539: Problem with post-procesing of "empty bean" serializer; was not calling 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 c9a0b5dc9e..53cd6831e9 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 @@ -442,21 +442,17 @@ public void serialize(Map value, JsonGenerator jgen, SerializerProvider pro if (!provider.isEnabled(SerializationFeature.WRITE_NULL_MAP_VALUES)) { suppressableValue = JsonInclude.Include.NON_NULL; } - - } - if (_filterId != null) { - serializeFilteredFields(value, jgen, provider, - findPropertyFilter(provider, _filterId, value), suppressableValue); - jgen.writeEndObject(); - return; + } else if (suppressableValue == JsonInclude.Include.ALWAYS) { + suppressableValue = null; } if (_sortKeys || provider.isEnabled(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS)) { value = _orderEntries(value); } - if (suppressableValue != null) { -// !!! TEST - serializeFields(value, jgen, provider); -// serializeOptionalFields(value, jgen, provider); + if (_filterId != null) { + serializeFilteredFields(value, jgen, provider, + findPropertyFilter(provider, _filterId, value), suppressableValue); + } else if (suppressableValue != null) { + serializeOptionalFields(value, jgen, provider, suppressableValue); } else if (_valueSerializer != null) { serializeFieldsUsing(value, jgen, provider, _valueSerializer); } else { @@ -478,14 +474,17 @@ public void serializeWithType(Map value, JsonGenerator jgen, SerializerProv if (!provider.isEnabled(SerializationFeature.WRITE_NULL_MAP_VALUES)) { suppressableValue = JsonInclude.Include.NON_NULL; } - + } else if (suppressableValue == JsonInclude.Include.ALWAYS) { + suppressableValue = null; } if (_sortKeys || provider.isEnabled(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS)) { value = _orderEntries(value); } - if (suppressableValue != null) { -// !!! TEST - serializeFields(value, jgen, provider); + if (_filterId != null) { + serializeFilteredFields(value, jgen, provider, + findPropertyFilter(provider, _filterId, value), suppressableValue); + } else if (suppressableValue != null) { + serializeOptionalFields(value, jgen, provider, suppressableValue); } else if (_valueSerializer != null) { serializeFieldsUsing(value, jgen, provider, _valueSerializer); } else { @@ -502,20 +501,19 @@ public void serializeWithType(Map value, JsonGenerator jgen, SerializerProv */ /** - * Method called to serialize fields, when the value type is not statically known. + * Method called to serialize fields, when the value type is not statically known; + * but we know that no value suppression is needed (which simplifies processing a bit) */ public void serializeFields(Map value, JsonGenerator jgen, SerializerProvider provider) throws IOException { // If value type needs polymorphic type handling, some more work needed: if (_valueTypeSerializer != null) { - serializeTypedFields(value, jgen, provider); + serializeTypedFields(value, jgen, provider, null); return; } final JsonSerializer keySerializer = _keySerializer; - final HashSet ignored = _ignoredEntries; - final boolean skipNulls = !provider.isEnabled(SerializationFeature.WRITE_NULL_MAP_VALUES); PropertySerializerMap serializers = _dynamicValueSerializers; @@ -526,8 +524,6 @@ public void serializeFields(Map value, JsonGenerator jgen, SerializerProvid if (keyElem == null) { provider.findNullKeySerializer(_keyType, _property).serialize(null, jgen, provider); } else { - // [JACKSON-314] 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); @@ -551,7 +547,7 @@ public void serializeFields(Map value, JsonGenerator jgen, SerializerProvid try { serializer.serialize(valueElem, jgen, provider); } catch (Exception e) { - // [JACKSON-55] Need to add reference information + // Add reference information String keyDesc = ""+keyElem; wrapAndThrow(provider, e, value, keyDesc); } @@ -559,6 +555,69 @@ public void serializeFields(Map value, JsonGenerator jgen, SerializerProvid } } + public void serializeOptionalFields(Map value, JsonGenerator jgen, SerializerProvider provider, + Object suppressableValue) + throws IOException + { + // If value type needs polymorphic type handling, some more work needed: + if (_valueTypeSerializer != null) { + serializeTypedFields(value, jgen, provider, suppressableValue); + return; + } + final HashSet ignored = _ignoredEntries; + PropertySerializerMap serializers = _dynamicValueSerializers; + + for (Map.Entry entry : value.entrySet()) { + // First find key serializer + final Object keyElem = entry.getKey(); + JsonSerializer keySerializer; + if (keyElem == null) { + keySerializer = provider.findNullKeySerializer(_keyType, _property); + } else { + if (ignored != null && ignored.contains(keyElem)) continue; + keySerializer = _keySerializer; + } + + // Then value serializer + final Object valueElem = entry.getValue(); + JsonSerializer valueSer; + if (valueElem == null) { + if (suppressableValue != null) { // all suppressions include null-suppression + continue; + } + valueSer = provider.getDefaultNullValueSerializer(); + } else { + valueSer = _valueSerializer; + if (valueSer == null) { + Class cc = valueElem.getClass(); + valueSer = serializers.serializerFor(cc); + if (valueSer == null) { + if (_valueType.hasGenericTypes()) { + valueSer = _findAndAddDynamic(serializers, + provider.constructSpecializedType(_valueType, cc), provider); + } else { + valueSer = _findAndAddDynamic(serializers, cc, provider); + } + serializers = _dynamicValueSerializers; + } + } + // also may need to skip non-empty values: + if ((suppressableValue == JsonInclude.Include.NON_EMPTY) + && valueSer.isEmpty(valueElem)) { + continue; + } + } + // and then serialize, if all went well + try { + keySerializer.serialize(keyElem, jgen, provider); + valueSer.serialize(valueElem, jgen, provider); + } catch (Exception e) { + String keyDesc = ""+keyElem; + wrapAndThrow(provider, e, value, keyDesc); + } + } + } + /** * Method called to serialize fields, when the value type is statically known, * so that value serializer is passed and does not need to be fetched from @@ -571,19 +630,17 @@ protected void serializeFieldsUsing(Map value, JsonGenerator jgen, Serializ final JsonSerializer keySerializer = _keySerializer; final HashSet ignored = _ignoredEntries; final TypeSerializer typeSer = _valueTypeSerializer; - final boolean skipNulls = !provider.isEnabled(SerializationFeature.WRITE_NULL_MAP_VALUES); for (Map.Entry entry : value.entrySet()) { - Object valueElem = entry.getValue(); Object keyElem = entry.getKey(); + if (ignored != null && ignored.contains(keyElem)) continue; + if (keyElem == null) { provider.findNullKeySerializer(_keyType, _property).serialize(null, jgen, provider); } else { - // [JACKSON-314] also may need to skip entries with null values - if (skipNulls && valueElem == null) continue; - if (ignored != null && ignored.contains(keyElem)) continue; keySerializer.serialize(keyElem, jgen, provider); } + final Object valueElem = entry.getValue(); if (valueElem == null) { provider.defaultSerializeNull(jgen); } else { @@ -594,7 +651,6 @@ protected void serializeFieldsUsing(Map value, JsonGenerator jgen, Serializ ser.serializeWithType(valueElem, jgen, provider, typeSer); } } catch (Exception e) { - // [JACKSON-55] Need to add reference information String keyDesc = ""+keyElem; wrapAndThrow(provider, e, value, keyDesc); } @@ -605,10 +661,8 @@ protected void serializeFieldsUsing(Map value, JsonGenerator jgen, Serializ /** * Helper method used when we have a JSON Filter to use for potentially * filtering out Map entries. - *

- * NOTE: initially only called externally, by AnyGetterWriter * - * @since 2.3 + * @since 2.5 */ public void serializeFilteredFields(Map value, JsonGenerator jgen, SerializerProvider provider, PropertyFilter filter, @@ -616,47 +670,57 @@ public void serializeFilteredFields(Map value, JsonGenerator jgen, Serializ throws IOException { final HashSet ignored = _ignoredEntries; - final boolean skipNulls = !provider.isEnabled(SerializationFeature.WRITE_NULL_MAP_VALUES); PropertySerializerMap serializers = _dynamicValueSerializers; final MapProperty prop = new MapProperty(_valueTypeSerializer, _property); for (Map.Entry entry : value.entrySet()) { - // First, serialize key + // First, serialize key; unless ignorable by key final Object keyElem = entry.getKey(); - final Object valueElem = entry.getValue(); - JsonSerializer keySer; + if (ignored != null && ignored.contains(keyElem)) continue; + + JsonSerializer keySerializer; if (keyElem == null) { - keySer = provider.findNullKeySerializer(_keyType, _property); + keySerializer = provider.findNullKeySerializer(_keyType, _property); } else { - // [JACKSON-314] 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; - keySer = _keySerializer; + keySerializer = _keySerializer; } + // or by value; nulls often suppressed + final Object valueElem = entry.getValue(); + JsonSerializer valueSer; // And then value if (valueElem == null) { + if (suppressableValue != null) { // all suppressions include null-suppression + continue; + } valueSer = provider.getDefaultNullValueSerializer(); } else { - Class cc = valueElem.getClass(); - valueSer = serializers.serializerFor(cc); + valueSer = _valueSerializer; if (valueSer == null) { - if (_valueType.hasGenericTypes()) { - valueSer = _findAndAddDynamic(serializers, - provider.constructSpecializedType(_valueType, cc), provider); - } else { - valueSer = _findAndAddDynamic(serializers, cc, provider); + Class cc = valueElem.getClass(); + valueSer = serializers.serializerFor(cc); + if (valueSer == null) { + if (_valueType.hasGenericTypes()) { + valueSer = _findAndAddDynamic(serializers, + provider.constructSpecializedType(_valueType, cc), provider); + } else { + valueSer = _findAndAddDynamic(serializers, cc, provider); + } + serializers = _dynamicValueSerializers; } - serializers = _dynamicValueSerializers; + } + // also may need to skip non-empty values: + if ((suppressableValue == JsonInclude.Include.NON_EMPTY) + && valueSer.isEmpty(valueElem)) { + continue; } } - prop.reset(keyElem, keySer, valueSer); + // and with that, ask filter to handle it + prop.reset(keyElem, keySerializer, valueSer); try { filter.serializeAsField(valueElem, jgen, provider, prop); } catch (Exception e) { - // [JACKSON-55] Need to add reference information String keyDesc = ""+keyElem; wrapAndThrow(provider, e, value, keyDesc); } @@ -665,62 +729,78 @@ 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); + PropertyFilter filter) throws IOException { + serializeFilteredFields(value, gen, provider, filter, + provider.isEnabled(SerializationFeature.WRITE_NULL_MAP_VALUES) ? null : JsonInclude.Include.NON_NULL); } - protected void serializeTypedFields(Map value, JsonGenerator gen, SerializerProvider provider) + /** + * @since 2.5 + */ + protected void serializeTypedFields(Map value, JsonGenerator gen, SerializerProvider provider, + Object suppressableValue) // since 2.5 throws IOException { - final JsonSerializer keySerializer = _keySerializer; - JsonSerializer prevValueSerializer = null; - Class prevValueClass = null; final HashSet ignored = _ignoredEntries; - final boolean skipNulls = !provider.isEnabled(SerializationFeature.WRITE_NULL_MAP_VALUES); - + PropertySerializerMap serializers = _dynamicValueSerializers; + for (Map.Entry entry : value.entrySet()) { - Object valueElem = entry.getValue(); - // First, serialize key Object keyElem = entry.getKey(); + JsonSerializer keySerializer; if (keyElem == null) { - provider.findNullKeySerializer(_keyType, _property).serialize(null, gen, provider); + keySerializer = provider.findNullKeySerializer(_keyType, _property); } 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, gen, provider); + keySerializer = _keySerializer; } + final Object valueElem = entry.getValue(); // And then value + JsonSerializer valueSer; if (valueElem == null) { + if (suppressableValue != null) { // all suppression include null suppression + continue; + } + valueSer = provider.getDefaultNullValueSerializer(); + keySerializer.serialize(keyElem, gen, provider); provider.defaultSerializeNull(gen); } else { + valueSer = _valueSerializer; Class cc = valueElem.getClass(); - JsonSerializer currSerializer; - if (cc == prevValueClass) { - currSerializer = prevValueSerializer; - } else { + valueSer = serializers.serializerFor(cc); + if (valueSer == null) { if (_valueType.hasGenericTypes()) { - currSerializer = provider.findValueSerializer(provider.constructSpecializedType(_valueType, cc), _property); + valueSer = _findAndAddDynamic(serializers, + provider.constructSpecializedType(_valueType, cc), provider); } else { - currSerializer = provider.findValueSerializer(cc, _property); + valueSer = _findAndAddDynamic(serializers, cc, provider); } - prevValueSerializer = currSerializer; - prevValueClass = cc; + serializers = _dynamicValueSerializers; } - try { - currSerializer.serializeWithType(valueElem, gen, provider, _valueTypeSerializer); - } catch (Exception e) { - // [JACKSON-55] Need to add reference information - String keyDesc = ""+keyElem; - wrapAndThrow(provider, e, value, keyDesc); + // also may need to skip non-empty values: + if ((suppressableValue == JsonInclude.Include.NON_EMPTY) + && valueSer.isEmpty(valueElem)) { + continue; } } + keySerializer.serialize(keyElem, gen, provider); + try { + valueSer.serializeWithType(valueElem, gen, provider, _valueTypeSerializer); + } catch (Exception e) { + String keyDesc = ""+keyElem; + wrapAndThrow(provider, e, value, keyDesc); + } } } + @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/test/java/com/fasterxml/jackson/databind/ser/TestMapSerialization.java b/src/test/java/com/fasterxml/jackson/databind/ser/TestMapSerialization.java index 8962c787fd..a491b48fad 100644 --- a/src/test/java/com/fasterxml/jackson/databind/ser/TestMapSerialization.java +++ b/src/test/java/com/fasterxml/jackson/databind/ser/TestMapSerialization.java @@ -108,7 +108,15 @@ public NoNullsStringMap add(String key, String value) { return this; } } - + + @JsonInclude(content=JsonInclude.Include.NON_EMPTY) + static class NoEmptyStringsMap extends LinkedHashMap { + public NoEmptyStringsMap add(String key, String value) { + put(key, value); + return this; + } + } + /* /********************************************************** /* Test methods @@ -217,9 +225,19 @@ public void testNonNullValueMap() throws IOException .add("a", "foo") .add("b", null) .add("c", "bar")); - assertEquals(aposToQuotes("{'stuff':{'a':'foo','c':'bar'}}"), json); + assertEquals(aposToQuotes("{'a':'foo','c':'bar'}"), json); } + // [databind#527] + public void testNonEmptyValueMap() throws IOException + { + String json = MAPPER.writeValueAsString(new NoEmptyStringsMap() + .add("a", "foo") + .add("b", "bar") + .add("c", "")); + assertEquals(aposToQuotes("{'a':'foo','b':'bar'}"), json); + } + // [databind#527] public void testNonNullValueMapViaProp() throws IOException {