diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 7b459e08..05147890 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -17,6 +17,8 @@ NOTE: Jackson 3.x components rely on 2.x annotations; there are no separate 2.22 (not yet released) #339: Add `OptBoolean` valued property "order" in `@JsonIncludeProperties` +#342: Add `@JsonTypeInfo.writeTypeIdForDefaultImpl` to allow skipping + writing of type id for values of default type 2.21 (18-Jan-2026) diff --git a/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java b/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java index cdb9167d..0f7a2716 100644 --- a/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java +++ b/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java @@ -67,9 +67,9 @@ public @interface JsonTypeInfo { /* - /********************************************************** + /********************************************************************** /* Value enumerations used for properties - /********************************************************** + /********************************************************************** */ /** @@ -256,9 +256,9 @@ public enum As { } /* - /********************************************************** + /********************************************************************** /* Annotation properties - /********************************************************** + /********************************************************************** */ /** @@ -339,23 +339,6 @@ public enum As { */ public boolean visible() default false; - /* - /********************************************************** - /* Helper classes - /********************************************************** - */ - - /** - * This marker class that is only to be used with defaultImpl - * annotation property, to indicate that there is no default implementation - * specified. - * - * @deprecated Since 2.5, use any Annotation type (such as {@link JsonTypeInfo}), - * if such behavior is needed; this is rarely necessary. - */ - @Deprecated // since 2.5 - public abstract static class None {} - /** * Specifies whether the type ID should be strictly required during polymorphic * deserialization of its subtypes. @@ -374,6 +357,44 @@ public abstract static class None {} */ public OptBoolean requireTypeIdForSubtypes() default OptBoolean.DEFAULT; + /** + * Property that defines whether serialization of type id should be done + * when the runtime type of the value is the same as {@link #defaultImpl()}. + * Skipping write can be useful since during deserialization, if no type id is present, + * {@code defaultImpl} is used as the fallback type -- so the type id is redundant + * for that type. + *

+ * When disabled ({@link OptBoolean#FALSE}), the type id will NOT be written + * if the actual runtime class of the value exactly matches {@code defaultImpl}. + * Subclasses of {@code defaultImpl} will still have their type id written. + *

+ * NOTE: support for this feature is only added in Jackson 3.x, specifically + * 3.2. It is not supported by Jackson 2.x. + *

+ * Default value is {@link OptBoolean#DEFAULT} (which means {@code TRUE}), + * preserving backwards-compatible behavior of always writing type id. + * + * @since 2.22 + */ + public OptBoolean writeTypeIdForDefaultImpl() default OptBoolean.DEFAULT; + + /* + /********************************************************************** + /* Helper classes + /********************************************************************** + */ + + /** + * This marker class that is only to be used with defaultImpl + * annotation property, to indicate that there is no default implementation + * specified. + * + * @deprecated Since 2.5, use any Annotation type (such as {@link JsonTypeInfo}), + * if such behavior is needed; this is rarely necessary. + */ + @Deprecated // since 2.5 + public abstract static class None {} + /* /********************************************************************** /* Value class used to enclose information, allow for @@ -399,14 +420,23 @@ public static class Value protected final boolean _idVisible; protected final Boolean _requireTypeIdForSubtypes; + /** + * @since 2.22 + */ + protected final Boolean _writeTypeIdForDefaultImpl; + /* /********************************************************************** /* Construction /********************************************************************** */ + /** + * @since 2.22 + */ protected Value(Id idType, As inclusionType, - String propertyName, Class defaultImpl, boolean idVisible, Boolean requireTypeIdForSubtypes) + String propertyName, Class defaultImpl, boolean idVisible, + Boolean requireTypeIdForSubtypes, Boolean writeTypeIdForDefaultImpl) { _defaultImpl = defaultImpl; _idType = idType; @@ -414,10 +444,25 @@ protected Value(Id idType, As inclusionType, _propertyName = propertyName; _idVisible = idVisible; _requireTypeIdForSubtypes = requireTypeIdForSubtypes; + _writeTypeIdForDefaultImpl = writeTypeIdForDefaultImpl; } - public static Value construct(Id idType, As inclusionType, + /** + * @deprecated Since 2.22 use the 7-argument overload + */ + @Deprecated + protected Value(Id idType, As inclusionType, String propertyName, Class defaultImpl, boolean idVisible, Boolean requireTypeIdForSubtypes) + { + this(idType, inclusionType, propertyName, defaultImpl, idVisible, requireTypeIdForSubtypes, null); + } + + /** + * @since 2.22 + */ + public static Value construct(Id idType, As inclusionType, + String propertyName, Class defaultImpl, boolean idVisible, + Boolean requireTypeIdForSubtypes, Boolean writeTypeIdForDefaultImpl) { // couple of overrides we need to apply here. First: if no propertyName specified, // use Id-specific property name @@ -433,7 +478,19 @@ public static Value construct(Id idType, As inclusionType, if ((defaultImpl == null) || defaultImpl.isAnnotation()) { defaultImpl = null; } - return new Value(idType, inclusionType, propertyName, defaultImpl, idVisible, requireTypeIdForSubtypes); + return new Value(idType, inclusionType, propertyName, defaultImpl, idVisible, + requireTypeIdForSubtypes, writeTypeIdForDefaultImpl); + } + + /** + * @deprecated Since 2.22 use the 7-argument overload + */ + @Deprecated + public static Value construct(Id idType, As inclusionType, + String propertyName, Class defaultImpl, boolean idVisible, + Boolean requireTypeIdForSubtypes) + { + return construct(idType, inclusionType, propertyName, defaultImpl, idVisible, requireTypeIdForSubtypes, null); } public static Value from(JsonTypeInfo src) { @@ -441,7 +498,9 @@ public static Value from(JsonTypeInfo src) { return null; } return construct(src.use(), src.include(), - src.property(), src.defaultImpl(), src.visible(), src.requireTypeIdForSubtypes().asBoolean()); + src.property(), src.defaultImpl(), src.visible(), + src.requireTypeIdForSubtypes().asBoolean(), + src.writeTypeIdForDefaultImpl().asBoolean()); } /* @@ -452,32 +511,47 @@ public static Value from(JsonTypeInfo src) { public Value withDefaultImpl(Class impl) { return (impl == _defaultImpl) ? this : - new Value(_idType, _inclusionType, _propertyName, impl, _idVisible, _requireTypeIdForSubtypes); + new Value(_idType, _inclusionType, _propertyName, impl, _idVisible, + _requireTypeIdForSubtypes, _writeTypeIdForDefaultImpl); } public Value withIdType(Id idType) { return (idType == _idType) ? this : - new Value(idType, _inclusionType, _propertyName, _defaultImpl, _idVisible, _requireTypeIdForSubtypes); + new Value(idType, _inclusionType, _propertyName, _defaultImpl, _idVisible, + _requireTypeIdForSubtypes, _writeTypeIdForDefaultImpl); } public Value withInclusionType(As inclusionType) { return (inclusionType == _inclusionType) ? this : - new Value(_idType, inclusionType, _propertyName, _defaultImpl, _idVisible, _requireTypeIdForSubtypes); + new Value(_idType, inclusionType, _propertyName, _defaultImpl, _idVisible, + _requireTypeIdForSubtypes, _writeTypeIdForDefaultImpl); } public Value withPropertyName(String propName) { return (propName == _propertyName) ? this : - new Value(_idType, _inclusionType, propName, _defaultImpl, _idVisible, _requireTypeIdForSubtypes); + new Value(_idType, _inclusionType, propName, _defaultImpl, _idVisible, + _requireTypeIdForSubtypes, _writeTypeIdForDefaultImpl); } public Value withIdVisible(boolean visible) { return (visible == _idVisible) ? this : - new Value(_idType, _inclusionType, _propertyName, _defaultImpl, visible, _requireTypeIdForSubtypes); + new Value(_idType, _inclusionType, _propertyName, _defaultImpl, visible, + _requireTypeIdForSubtypes, _writeTypeIdForDefaultImpl); } - + public Value withRequireTypeIdForSubtypes(Boolean requireTypeIdForSubtypes) { return (_requireTypeIdForSubtypes == requireTypeIdForSubtypes) ? this : - new Value(_idType, _inclusionType, _propertyName, _defaultImpl, _idVisible, requireTypeIdForSubtypes); + new Value(_idType, _inclusionType, _propertyName, _defaultImpl, _idVisible, + requireTypeIdForSubtypes, _writeTypeIdForDefaultImpl); + } + + /** + * @since 2.22 + */ + public Value withWriteTypeIdForDefaultImpl(Boolean writeTypeIdForDefaultImpl) { + return (_writeTypeIdForDefaultImpl == writeTypeIdForDefaultImpl) ? this : + new Value(_idType, _inclusionType, _propertyName, _defaultImpl, _idVisible, + _requireTypeIdForSubtypes, writeTypeIdForDefaultImpl); } /* @@ -498,6 +572,18 @@ public Class valueFor() { public boolean getIdVisible() { return _idVisible; } public Boolean getRequireTypeIdForSubtypes() { return _requireTypeIdForSubtypes; } + /** + * @since 2.22 + */ + public Boolean getWriteTypeIdForDefaultImpl() { return _writeTypeIdForDefaultImpl; } + + /** + * @since 2.22 + */ + public boolean shouldWriteTypeIdForDefaultImpl() { + return (_writeTypeIdForDefaultImpl == null) || _writeTypeIdForDefaultImpl.booleanValue(); + } + /** * Static helper method for simple(r) checking of whether there's a Value instance * that indicates that polymorphic handling is (to be) enabled. @@ -516,11 +602,11 @@ public static boolean isEnabled(JsonTypeInfo.Value v) { @Override public String toString() { - return String.format("JsonTypeInfo.Value(idType=%s,includeAs=%s,propertyName=%s,defaultImpl=%s,idVisible=%s" - + ",requireTypeIdForSubtypes=%s)", + return String.format("JsonTypeInfo.Value(idType=%s,includeAs=%s,propertyName=%s,defaultImpl=%s,idVisible=%s" + + ",requireTypeIdForSubtypes=%s,writeTypeIdForDefaultImpl=%s)", _idType, _inclusionType, _propertyName, ((_defaultImpl == null) ? "NULL" : _defaultImpl.getName()), - _idVisible, _requireTypeIdForSubtypes); + _idVisible, _requireTypeIdForSubtypes, _writeTypeIdForDefaultImpl); } @Override @@ -530,8 +616,9 @@ public int hashCode() { hashCode = 31 * hashCode + (_inclusionType != null ? _inclusionType.hashCode() : 0); hashCode = 31 * hashCode + (_propertyName != null ? _propertyName.hashCode() : 0); hashCode = 31 * hashCode + (_defaultImpl != null ? _defaultImpl.hashCode() : 0); - hashCode = 31 * hashCode + (_requireTypeIdForSubtypes ? 11 : -17); + hashCode = 31 * hashCode + Objects.hashCode(_requireTypeIdForSubtypes); hashCode = 31 * hashCode + (_idVisible ? 11 : -17); + hashCode = 31 * hashCode + Objects.hashCode(_writeTypeIdForDefaultImpl); return hashCode; } @@ -551,6 +638,7 @@ private static boolean _equals(Value a, Value b) && (a._idVisible == b._idVisible) && Objects.equals(a._propertyName, b._propertyName) && Objects.equals(a._requireTypeIdForSubtypes, b._requireTypeIdForSubtypes) + && Objects.equals(a._writeTypeIdForDefaultImpl, b._writeTypeIdForDefaultImpl) ; } } diff --git a/src/test/java/com/fasterxml/jackson/annotation/JsonIncludePropertiesTest.java b/src/test/java/com/fasterxml/jackson/annotation/JsonIncludePropertiesTest.java index 3b88b8e0..8804f196 100644 --- a/src/test/java/com/fasterxml/jackson/annotation/JsonIncludePropertiesTest.java +++ b/src/test/java/com/fasterxml/jackson/annotation/JsonIncludePropertiesTest.java @@ -70,7 +70,7 @@ public void testWithOverridesAll() { @Test public void testWithOverridesEmpty() { JsonIncludeProperties.Value v = JsonIncludeProperties.Value.from(Bogus.class.getAnnotation(JsonIncludeProperties.class)); - v = v.withOverrides(new JsonIncludeProperties.Value(Collections.emptySet())); + v = v.withOverrides(new JsonIncludeProperties.Value(Collections.emptySet(), false)); Set included = v.getIncluded(); assertEquals(0, included.size()); } @@ -78,7 +78,7 @@ public void testWithOverridesEmpty() { @Test public void testWithOverridesMerge() { JsonIncludeProperties.Value v = JsonIncludeProperties.Value.from(Bogus.class.getAnnotation(JsonIncludeProperties.class)); - v = v.withOverrides(new JsonIncludeProperties.Value(_set("foo"))); + v = v.withOverrides(new JsonIncludeProperties.Value(_set("foo"), false)); Set included = v.getIncluded(); assertEquals(1, included.size()); assertEquals(_set("foo"), included); diff --git a/src/test/java/com/fasterxml/jackson/annotation/JsonTypeInfoTest.java b/src/test/java/com/fasterxml/jackson/annotation/JsonTypeInfoTest.java index 6c8a2bfa..32862640 100644 --- a/src/test/java/com/fasterxml/jackson/annotation/JsonTypeInfoTest.java +++ b/src/test/java/com/fasterxml/jackson/annotation/JsonTypeInfoTest.java @@ -23,6 +23,15 @@ private final static class Anno2 { } defaultImpl = Void.class) private final static class Anno3 { } + @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, visible = true, + defaultImpl = Void.class, + writeTypeIdForDefaultImpl = OptBoolean.FALSE) + private final static class Anno4 { } + + @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, + writeTypeIdForDefaultImpl = OptBoolean.TRUE) + private final static class Anno5 { } + @Test public void testEmpty() { // 07-Mar-2017, tatu: Important to distinguish "none" from 'empty' value... @@ -56,8 +65,8 @@ public void testFromAnnotation() throws Exception assertFalse(v1.equals(v2)); assertFalse(v2.equals(v1)); - assertEquals("JsonTypeInfo.Value(idType=CLASS,includeAs=PROPERTY,propertyName=@class,defaultImpl=NULL,idVisible=true,requireTypeIdForSubtypes=true)", v1.toString()); - assertEquals("JsonTypeInfo.Value(idType=NAME,includeAs=EXTERNAL_PROPERTY,propertyName=ext,defaultImpl=java.lang.Void,idVisible=false,requireTypeIdForSubtypes=false)", v2.toString()); + assertEquals("JsonTypeInfo.Value(idType=CLASS,includeAs=PROPERTY,propertyName=@class,defaultImpl=NULL,idVisible=true,requireTypeIdForSubtypes=true,writeTypeIdForDefaultImpl=null)", v1.toString()); + assertEquals("JsonTypeInfo.Value(idType=NAME,includeAs=EXTERNAL_PROPERTY,propertyName=ext,defaultImpl=java.lang.Void,idVisible=false,requireTypeIdForSubtypes=false,writeTypeIdForDefaultImpl=null)", v2.toString()); // Let's also verify JDK serializability byte[] b = jdkSerialize(v1); @@ -115,7 +124,100 @@ public void testDefaultValueForRequireTypeIdForSubtypes() { assertNull(v3.getRequireTypeIdForSubtypes()); // toString() - assertEquals("JsonTypeInfo.Value(idType=NAME,includeAs=EXTERNAL_PROPERTY,propertyName=ext," - + "defaultImpl=java.lang.Void,idVisible=false,requireTypeIdForSubtypes=null)", v3.toString()); + assertEquals("JsonTypeInfo.Value(idType=NAME,includeAs=EXTERNAL_PROPERTY,propertyName=ext," + + "defaultImpl=java.lang.Void,idVisible=false,requireTypeIdForSubtypes=null,writeTypeIdForDefaultImpl=null)", v3.toString()); + } + + // [annotations#342] + @Test + public void testWriteTypeIdForDefaultImplFromAnnotation() { + // Anno4: writeTypeIdForDefaultImpl = FALSE + JsonTypeInfo.Value v4 = JsonTypeInfo.Value.from(Anno4.class.getAnnotation(JsonTypeInfo.class)); + assertEquals(Boolean.FALSE, v4.getWriteTypeIdForDefaultImpl()); + assertFalse(v4.shouldWriteTypeIdForDefaultImpl()); + + // Anno5: writeTypeIdForDefaultImpl = TRUE + JsonTypeInfo.Value v5 = JsonTypeInfo.Value.from(Anno5.class.getAnnotation(JsonTypeInfo.class)); + assertEquals(Boolean.TRUE, v5.getWriteTypeIdForDefaultImpl()); + assertTrue(v5.shouldWriteTypeIdForDefaultImpl()); + + // Anno3: writeTypeIdForDefaultImpl not set (DEFAULT -> null) + JsonTypeInfo.Value v3 = JsonTypeInfo.Value.from(Anno3.class.getAnnotation(JsonTypeInfo.class)); + assertNull(v3.getWriteTypeIdForDefaultImpl()); + // default should be treated as "write" (true) + assertTrue(v3.shouldWriteTypeIdForDefaultImpl()); + } + + // [annotations#342] + @Test + public void testWithWriteTypeIdForDefaultImpl() { + JsonTypeInfo.Value empty = JsonTypeInfo.Value.EMPTY; + assertNull(empty.getWriteTypeIdForDefaultImpl()); + assertTrue(empty.shouldWriteTypeIdForDefaultImpl()); + + // Mutate to FALSE + JsonTypeInfo.Value vFalse = empty.withWriteTypeIdForDefaultImpl(Boolean.FALSE); + assertEquals(Boolean.FALSE, vFalse.getWriteTypeIdForDefaultImpl()); + assertFalse(vFalse.shouldWriteTypeIdForDefaultImpl()); + + // Mutate to TRUE + JsonTypeInfo.Value vTrue = empty.withWriteTypeIdForDefaultImpl(Boolean.TRUE); + assertEquals(Boolean.TRUE, vTrue.getWriteTypeIdForDefaultImpl()); + assertTrue(vTrue.shouldWriteTypeIdForDefaultImpl()); + + // Mutate back to null + JsonTypeInfo.Value vNull = vFalse.withWriteTypeIdForDefaultImpl(null); + assertNull(vNull.getWriteTypeIdForDefaultImpl()); + assertTrue(vNull.shouldWriteTypeIdForDefaultImpl()); + + // Same value returns same instance + assertSame(vFalse, vFalse.withWriteTypeIdForDefaultImpl(Boolean.FALSE)); + assertSame(vTrue, vTrue.withWriteTypeIdForDefaultImpl(Boolean.TRUE)); + } + + // [annotations#342] + @Test + public void testWriteTypeIdForDefaultImplEqualsAndHashCode() { + JsonTypeInfo.Value v1 = JsonTypeInfo.Value.EMPTY.withWriteTypeIdForDefaultImpl(Boolean.TRUE); + JsonTypeInfo.Value v2 = JsonTypeInfo.Value.EMPTY.withWriteTypeIdForDefaultImpl(Boolean.TRUE); + JsonTypeInfo.Value v3 = JsonTypeInfo.Value.EMPTY.withWriteTypeIdForDefaultImpl(Boolean.FALSE); + JsonTypeInfo.Value vNull = JsonTypeInfo.Value.EMPTY.withWriteTypeIdForDefaultImpl(null); + + assertEquals(v1, v2); + assertEquals(v1.hashCode(), v2.hashCode()); + + assertNotEquals(v1, v3); + assertNotEquals(v1, vNull); + assertNotEquals(v3, vNull); + } + + // [annotations#342] + @Test + public void testWriteTypeIdForDefaultImplToString() { + JsonTypeInfo.Value vFalse = JsonTypeInfo.Value.EMPTY.withWriteTypeIdForDefaultImpl(Boolean.FALSE); + assertTrue(vFalse.toString().contains("writeTypeIdForDefaultImpl=false")); + + JsonTypeInfo.Value vTrue = JsonTypeInfo.Value.EMPTY.withWriteTypeIdForDefaultImpl(Boolean.TRUE); + assertTrue(vTrue.toString().contains("writeTypeIdForDefaultImpl=true")); + } + + // [annotations#342] + @Test + public void testWriteTypeIdForDefaultImplConstruct() { + JsonTypeInfo.Value v = JsonTypeInfo.Value.construct( + JsonTypeInfo.Id.CLASS, JsonTypeInfo.As.PROPERTY, + null, Void.class, false, null, Boolean.FALSE); + assertEquals(Boolean.FALSE, v.getWriteTypeIdForDefaultImpl()); + assertFalse(v.shouldWriteTypeIdForDefaultImpl()); + } + + // [annotations#342] + @Test + public void testWriteTypeIdForDefaultImplSerialization() throws Exception { + JsonTypeInfo.Value v = JsonTypeInfo.Value.EMPTY.withWriteTypeIdForDefaultImpl(Boolean.FALSE); + byte[] b = jdkSerialize(v); + JsonTypeInfo.Value deser = jdkDeserialize(b); + assertEquals(v, deser); + assertEquals(Boolean.FALSE, deser.getWriteTypeIdForDefaultImpl()); } }