diff --git a/release-notes/VERSION b/release-notes/VERSION index 4722e00d..e4b04c27 100644 --- a/release-notes/VERSION +++ b/release-notes/VERSION @@ -14,10 +14,11 @@ NOTE: Annotations module will never contain changes in patch versions, 2.9.0 (not yet released) #103: Add `JsonInclude.Include.CUSTOM`, properties for specifying filter(s) to use -#104: Add new properties in `@JsonSetter`: `merge`, `null`/`contentNulls` +#104: Add new properties in `@JsonSetter`: `nulls`/`contentNulls` #105: Add `@JsonFormat.lenient` to allow configuring lenience of date/time deserializers #108: Allow `@JsonValue` on fields #109: Add `enabled` for `@JsonAnyGetter`, `@JsonAnySetter`, to allow disabling via mix-ins +#113: Add `@JsonMerge` to support (deep) merging of properties - Allow use of `@JsonView` on classes, to specify Default View to use on non-annotated properties. diff --git a/src/main/java/com/fasterxml/jackson/annotation/JsonMerge.java b/src/main/java/com/fasterxml/jackson/annotation/JsonMerge.java new file mode 100644 index 00000000..1302d397 --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/annotation/JsonMerge.java @@ -0,0 +1,51 @@ +package com.fasterxml.jackson.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to specify whether annotated property value should use "merging" approach, + * in which current value is first accessed (with a getter or field) and then modified + * with incoming data, or not: if not, assignment happens without considering current state. + *

+ * Merging is only option if there is a way to introspect current state: + * if there is accessor (getter, field) to use. + * Merging can not be enabled if no accessor exists + * or if assignment occurs using a Creator setter (constructor + * or factory method), since there is no instance with state to introspect. + * Merging also only has actual effect for structured types where there is an + * obvious way to update a state (for example, POJOs have default values for properties, + * and {@link java.util.Collection}s and {@link java.util.Map}s may have existing + * elements; whereas scalar types do not such state: an int has a value, + * but no obvious and non-ambiguous way to merge state. + *

+ * Merging is applied by using a deserialization method that accepts existing state + * as an argument: it is then up to JsonDeserializer implementation + * to use that base state in a way that makes sense without further configuration. + * For structured types this is usually obvious; and for scalar types not -- if + * no obvious method exists, merging is not allowed; deserializer may choose to + * either quietly ignore it, or throw an exception. + *

+ * Note that use of merging usually adds some processing overhead since it adds + * an extra step of accessing the current state before assignment. + *

+ * Note also that "root values" (values directly deserialized and not reached + * via POJO properties) can not use this annotation; instead, ObjectMapper + * and Object have "updating reader" operations. + *

+ * Default value is {@link OptBoolean#TRUE}, that is, merging is enabled. + * + * @since 2.9 + */ +@Target({ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +@JacksonAnnotation +public @interface JsonMerge +{ + /** + * Whether merging should or should not be enabled for the annotated property. + */ + OptBoolean value() default OptBoolean.TRUE; +} diff --git a/src/main/java/com/fasterxml/jackson/annotation/JsonSetter.java b/src/main/java/com/fasterxml/jackson/annotation/JsonSetter.java index 704d5643..40a5d173 100644 --- a/src/main/java/com/fasterxml/jackson/annotation/JsonSetter.java +++ b/src/main/java/com/fasterxml/jackson/annotation/JsonSetter.java @@ -9,8 +9,6 @@ * {@link JsonProperty} annotation; * or (as of 2.9 and later), specify additional aspects of the * assigning property a value during serialization. - *

- * */ @Target({ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER}) // ^^^ allowed on Fields, (constructor) parameters since 2.9 @@ -47,35 +45,6 @@ * is usually {@link Nulls#SET}, meaning that the `null` is included as usual. */ Nulls contentNulls() default Nulls.DEFAULT; - - /** - * Specifies whether property value should use "merging" approach, in which - * current value is first accessed (with a getter), or not; if not, - * assignment happens without considering current state. - * Merging is not an option if there is no way to introspect - * current state: for example, if there is no - * accessor to use, or if assignment occurs using a Creator setter (constructor - * or factory method), since there is no instance with state to introspect. - * Merging also only has actual effect for structured types where there is an - * obvious way to update a state (for example, POJOs have default values for properties, - * and {@link java.util.Collection}s and {@link java.util.Map}s may have existing - * elements; whereas scalar types do not such state: an int has a value, - * but no obvious and non-ambiguous way to merge state. - *

- * Merging is applied by using a deserialization method that accepts existing state - * as an argument: it is then up to JsonDeserializer implementation - * to use that base state in a way that makes sense without further configuration. - * For structured types this is usually obvious; and for scalar types not -- if - * no obvious method exists, merging is not allowed; deserializer may choose to - * either quietly ignore it, or throw an exception. - *

- * Note also that use of merging usually adds some processing overhead since it adds - * an extra step of accessing the current state before assignment. - *

- * Default value is to "use defaults"; in absence of any explicit configuration - * this would mean {@link OptBoolean#FALSE}, that is, merging is not enabled. - */ - OptBoolean merge() default OptBoolean.DEFAULT; /* /********************************************************** @@ -144,8 +113,6 @@ public static class Value { private static final long serialVersionUID = 1L; - private final Boolean _merge; - private final Nulls _nulls; private final Nulls _contentNulls; @@ -153,17 +120,16 @@ public static class Value /** * Default instance used in place of "default settings". */ - protected final static Value EMPTY = new Value(null, Nulls.DEFAULT, Nulls.DEFAULT); + protected final static Value EMPTY = new Value(Nulls.DEFAULT, Nulls.DEFAULT); - protected Value(Boolean merge, Nulls nulls, Nulls contentNulls) { - _merge = merge; + protected Value(Nulls nulls, Nulls contentNulls) { _nulls = nulls; _contentNulls = contentNulls; } // for JDK serialization protected Object readResolve() { - if (_empty(_merge, _nulls, _contentNulls)) { + if (_empty(_nulls, _contentNulls)) { return EMPTY; } return this; @@ -173,8 +139,7 @@ public static Value from(JsonSetter src) { if (src == null) { return EMPTY; } - return construct(src.merge().asBoolean(), - src.nulls(), src.contentNulls()); + return construct(src.nulls(), src.contentNulls()); } /** @@ -184,24 +149,22 @@ public static Value from(JsonSetter src) { * methods, as this factory method may need to be changed if new properties * are added in {@link JsonIgnoreProperties} annotation. */ - public static Value construct(Boolean merge, Nulls nulls, Nulls contentNulls) { + public static Value construct(Nulls nulls, Nulls contentNulls) { if (nulls == null) { nulls = Nulls.DEFAULT; } if (contentNulls == null) { contentNulls = Nulls.DEFAULT; } - if (_empty(merge, nulls, contentNulls)) { + if (_empty(nulls, contentNulls)) { return EMPTY; } - return new Value(merge, nulls, contentNulls); + return new Value(nulls, contentNulls); } /** * Accessor for default instances which has "empty" settings; that is: *

@@ -226,29 +189,17 @@ public static Value merge(Value base, Value overrides) } public static Value forValueNulls(Nulls nulls) { - return construct(null, nulls, Nulls.DEFAULT); + return construct(nulls, Nulls.DEFAULT); } public static Value forValueNulls(Nulls nulls, Nulls contentNulls) { - return construct(null, nulls, contentNulls); + return construct(nulls, contentNulls); } public static Value forContentNulls(Nulls nulls) { - return construct(null, Nulls.DEFAULT, nulls); - } - - public static Value forMerging(Boolean merge) { - return construct(merge, Nulls.DEFAULT, Nulls.DEFAULT); + return construct(Nulls.DEFAULT, nulls); } - public static Value forMerging() { - return construct(Boolean.TRUE, Nulls.DEFAULT, Nulls.DEFAULT); - } - - public static Value forNonMerging() { - return construct(Boolean.FALSE, Nulls.DEFAULT, Nulls.DEFAULT); - } - /** * Mutant factory method that merges values of this value with given override * values, so that any explicitly defined inclusion in overrides has precedence over @@ -259,13 +210,9 @@ public Value withOverrides(Value overrides) { if ((overrides == null) || (overrides == EMPTY)) { return this; } - Boolean merge = overrides._merge; Nulls nulls = overrides._nulls; Nulls contentNulls = overrides._contentNulls; - if (merge == null) { - merge = _merge; - } if (nulls == Nulls.DEFAULT) { nulls = _nulls; } @@ -273,10 +220,10 @@ public Value withOverrides(Value overrides) { contentNulls = _contentNulls; } - if ((merge == _merge) && (nulls == _nulls) && (contentNulls == _contentNulls)) { + if ((nulls == _nulls) && (contentNulls == _contentNulls)) { return this; } - return construct(merge, nulls, contentNulls); + return construct(nulls, contentNulls); } public Value withValueNulls(Nulls nulls) { @@ -286,7 +233,7 @@ public Value withValueNulls(Nulls nulls) { if (nulls == _nulls) { return this; } - return construct(_merge, nulls, _contentNulls); + return construct(nulls, _contentNulls); } public Value withValueNulls(Nulls valueNulls, Nulls contentNulls) { @@ -299,7 +246,7 @@ public Value withValueNulls(Nulls valueNulls, Nulls contentNulls) { if ((valueNulls == _nulls) && (contentNulls == _contentNulls)) { return this; } - return construct(_merge, valueNulls, contentNulls); + return construct(valueNulls, contentNulls); } public Value withContentNulls(Nulls nulls) { @@ -309,18 +256,11 @@ public Value withContentNulls(Nulls nulls) { if (nulls == _contentNulls) { return this; } - return construct(_merge, _nulls, nulls); - } - - public Value withMerge(Boolean merge) { - return (merge == _merge) ? this : construct(merge, _nulls, _contentNulls); + return construct(_nulls, nulls); } public Nulls getValueNulls() { return _nulls; } public Nulls getContentNulls() { return _contentNulls; } - public Boolean getMerge() { return _merge; } - - public boolean shouldMerge() { return (_merge != null) && _merge.booleanValue(); } /** * Returns same as {@link #getValueNulls()} unless value would be @@ -345,15 +285,13 @@ public Class valueFor() { @Override public String toString() { - return String.format("JsonSetter.Value(merge=%s,valueNulls=%s,contentNulls=%s)", - _merge, _nulls, _contentNulls); + return String.format("JsonSetter.Value(valueNulls=%s,contentNulls=%s)", + _nulls, _contentNulls); } @Override public int hashCode() { - return (_merge == null) ? 1 : (_merge.booleanValue() ? 30 : -30) - + _nulls.ordinal() - + (_contentNulls.ordinal() << 2); + return _nulls.ordinal() + (_contentNulls.ordinal() << 2); } @Override @@ -362,16 +300,14 @@ public boolean equals(Object o) { if (o == null) return false; if (o.getClass() == getClass()) { Value other = (Value) o; - return (other._merge == _merge) - && (other._nulls == _nulls) + return (other._nulls == _nulls) && (other._contentNulls == _contentNulls); } return false; } - private static boolean _empty(Boolean merge, Nulls nulls, Nulls contentNulls) { - return (merge == null) - && (nulls == Nulls.DEFAULT) + private static boolean _empty(Nulls nulls, Nulls contentNulls) { + return (nulls == Nulls.DEFAULT) && (contentNulls == Nulls.DEFAULT); } } diff --git a/src/test/java/com/fasterxml/jackson/annotation/JsonSetterTest.java b/src/test/java/com/fasterxml/jackson/annotation/JsonSetterTest.java index 1b461f10..5fcd6026 100644 --- a/src/test/java/com/fasterxml/jackson/annotation/JsonSetterTest.java +++ b/src/test/java/com/fasterxml/jackson/annotation/JsonSetterTest.java @@ -5,7 +5,7 @@ public class JsonSetterTest extends TestBase { private final static class Bogus { - @JsonSetter(nulls=Nulls.FAIL, contentNulls=Nulls.SKIP, merge=OptBoolean.TRUE) + @JsonSetter(nulls=Nulls.FAIL, contentNulls=Nulls.SKIP) public int field; } @@ -15,8 +15,6 @@ public void testEmpty() { assertEquals(JsonSetter.Nulls.DEFAULT, EMPTY.getValueNulls()); assertEquals(JsonSetter.Nulls.DEFAULT, EMPTY.getContentNulls()); - assertNull(EMPTY.getMerge()); - assertFalse(EMPTY.shouldMerge()); assertEquals(JsonSetter.class, EMPTY.valueFor()); @@ -25,7 +23,7 @@ public void testEmpty() } public void testStdMethods() { - assertEquals("JsonSetter.Value(merge=null,valueNulls=DEFAULT,contentNulls=DEFAULT)", + assertEquals("JsonSetter.Value(valueNulls=DEFAULT,contentNulls=DEFAULT)", EMPTY.toString()); int x = EMPTY.hashCode(); if (x == 0) { // no fixed value, but should not evalute to 0 @@ -44,12 +42,11 @@ public void testFromAnnotation() throws Exception JsonSetter.Value v = JsonSetter.Value.from(ann); assertEquals(JsonSetter.Nulls.FAIL, v.getValueNulls()); assertEquals(JsonSetter.Nulls.SKIP, v.getContentNulls()); - assertEquals(Boolean.TRUE, v.getMerge()); } public void testConstruct() throws Exception { - JsonSetter.Value v = JsonSetter.Value.construct(null, null, null); + JsonSetter.Value v = JsonSetter.Value.construct(null, null); assertSame(EMPTY, v); } @@ -58,25 +55,12 @@ public void testFactories() throws Exception JsonSetter.Value v = JsonSetter.Value.forContentNulls(Nulls.SET); assertEquals(Nulls.DEFAULT, v.getValueNulls()); assertEquals(Nulls.SET, v.getContentNulls()); - assertNull(v.getMerge()); assertEquals(Nulls.SET, v.nonDefaultContentNulls()); JsonSetter.Value skip = JsonSetter.Value.forValueNulls(Nulls.SKIP); assertEquals(Nulls.SKIP, skip.getValueNulls()); assertEquals(Nulls.DEFAULT, skip.getContentNulls()); assertEquals(Nulls.SKIP, skip.nonDefaultValueNulls()); - assertNull(skip.getMerge()); - - JsonSetter.Value merging = JsonSetter.Value.forMerging(); - assertEquals(Nulls.DEFAULT, merging.getValueNulls()); - assertEquals(Nulls.DEFAULT, merging.getContentNulls()); - assertEquals(Boolean.TRUE, merging.getMerge()); - - assertFalse(skip.equals(merging)); - assertFalse(merging.equals(skip)); - - assertFalse(merging.equals(EMPTY)); - assertFalse(EMPTY.equals(merging)); } public void testSimpleMerge() @@ -85,15 +69,6 @@ public void testSimpleMerge() assertEquals(Nulls.SKIP, v.getContentNulls()); v = v.withValueNulls(Nulls.FAIL); assertEquals(Nulls.FAIL, v.getValueNulls()); - v = v.withMerge(Boolean.FALSE); - assertEquals(Boolean.FALSE, v.getMerge()); - - JsonSetter.Value override = JsonSetter.Value.forMerging(Boolean.TRUE); - JsonSetter.Value merged = JsonSetter.Value.merge(v, override); - assertEquals(Boolean.TRUE, merged.getMerge()); - assertEquals(Nulls.SKIP, merged.getContentNulls()); - assertEquals(Nulls.FAIL, merged.getValueNulls()); - assertTrue(merged.shouldMerge()); } public void testWithMethods()