From a0b586ccb74d83751da997cd3848285740ff568a Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Thu, 19 Mar 2026 20:01:37 -0700 Subject: [PATCH 1/9] Add property in `@JsonTypeInfo` for skipping writing of default type id --- .../jackson/annotation/JsonTypeInfo.java | 90 ++++++++++++++++--- 1 file changed, 78 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java b/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java index cdb9167d..b346a5b2 100644 --- a/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java +++ b/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java @@ -374,6 +374,24 @@ public abstract static class None {} */ public OptBoolean requireTypeIdForSubtypes() default OptBoolean.DEFAULT; + /** + * Property that defines whether serialization of type id should be skipped + * when the runtime type of the value is the same as {@link #defaultImpl()}. + * This is useful because 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 enabled ({@link OptBoolean#TRUE}), 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. + *

+ * Default value is {@link OptBoolean#DEFAULT} (which means {@code FALSE}), + * preserving backwards-compatible behavior of always writing type id. + * + * @since 2.22 + */ + public OptBoolean skipWriteForDefaultImpl() default OptBoolean.DEFAULT; + /* /********************************************************************** /* Value class used to enclose information, allow for @@ -399,6 +417,11 @@ public static class Value protected final boolean _idVisible; protected final Boolean _requireTypeIdForSubtypes; + /** + * @since 2.21 + */ + protected final Boolean _skipWriteForDefaultImpl; + /* /********************************************************************** /* Construction @@ -407,6 +430,16 @@ public static class Value protected Value(Id idType, As inclusionType, String propertyName, Class defaultImpl, boolean idVisible, Boolean requireTypeIdForSubtypes) + { + this(idType, inclusionType, propertyName, defaultImpl, idVisible, requireTypeIdForSubtypes, null); + } + + /** + * @since 2.21 + */ + protected Value(Id idType, As inclusionType, + String propertyName, Class defaultImpl, boolean idVisible, + Boolean requireTypeIdForSubtypes, Boolean skipWriteForDefaultImpl) { _defaultImpl = defaultImpl; _idType = idType; @@ -414,10 +447,25 @@ protected Value(Id idType, As inclusionType, _propertyName = propertyName; _idVisible = idVisible; _requireTypeIdForSubtypes = requireTypeIdForSubtypes; + _skipWriteForDefaultImpl = skipWriteForDefaultImpl; } + /** + * @deprecated Since 2.21 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); + } + + /** + * @since 2.21 + */ + public static Value construct(Id idType, As inclusionType, + String propertyName, Class defaultImpl, boolean idVisible, + Boolean requireTypeIdForSubtypes, Boolean skipWriteForDefaultImpl) { // couple of overrides we need to apply here. First: if no propertyName specified, // use Id-specific property name @@ -433,7 +481,8 @@ 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, skipWriteForDefaultImpl); } public static Value from(JsonTypeInfo src) { @@ -441,7 +490,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.skipWriteForDefaultImpl().asBoolean()); } /* @@ -452,32 +503,40 @@ 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, _skipWriteForDefaultImpl); } 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, _skipWriteForDefaultImpl); } 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, _skipWriteForDefaultImpl); } 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, _skipWriteForDefaultImpl); } 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, _skipWriteForDefaultImpl); } - + 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, _skipWriteForDefaultImpl); + } + + /** + * @since 2.21 + */ + public Value withSkipWriteForDefaultImpl(Boolean skipWrite) { + return (_skipWriteForDefaultImpl == skipWrite) ? this : + new Value(_idType, _inclusionType, _propertyName, _defaultImpl, _idVisible, _requireTypeIdForSubtypes, skipWrite); } /* @@ -498,6 +557,11 @@ public Class valueFor() { public boolean getIdVisible() { return _idVisible; } public Boolean getRequireTypeIdForSubtypes() { return _requireTypeIdForSubtypes; } + /** + * @since 2.21 + */ + public Boolean getSkipWriteForDefaultImpl() { return _skipWriteForDefaultImpl; } + /** * 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 +580,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,skipWriteForDefaultImpl=%s)", _idType, _inclusionType, _propertyName, ((_defaultImpl == null) ? "NULL" : _defaultImpl.getName()), - _idVisible, _requireTypeIdForSubtypes); + _idVisible, _requireTypeIdForSubtypes, _skipWriteForDefaultImpl); } @Override @@ -532,6 +596,7 @@ public int hashCode() { hashCode = 31 * hashCode + (_defaultImpl != null ? _defaultImpl.hashCode() : 0); hashCode = 31 * hashCode + (_requireTypeIdForSubtypes ? 11 : -17); hashCode = 31 * hashCode + (_idVisible ? 11 : -17); + hashCode = 31 * hashCode + (_skipWriteForDefaultImpl != null ? _skipWriteForDefaultImpl.hashCode() : 0); return hashCode; } @@ -551,6 +616,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._skipWriteForDefaultImpl, b._skipWriteForDefaultImpl) ; } } From b91f26c33a607f5bab02d8b9d4e46ffee72dc3f1 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Tue, 31 Mar 2026 16:53:11 -0700 Subject: [PATCH 2/9] Rename --- release-notes/VERSION-2.x | 2 + .../jackson/annotation/JsonTypeInfo.java | 68 +++++++++++-------- .../annotation/JsonIncludePropertiesTest.java | 4 +- 3 files changed, 42 insertions(+), 32 deletions(-) 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 b346a5b2..41f9762a 100644 --- a/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java +++ b/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java @@ -375,22 +375,22 @@ public abstract static class None {} public OptBoolean requireTypeIdForSubtypes() default OptBoolean.DEFAULT; /** - * Property that defines whether serialization of type id should be skipped + * Property that defines whether serialization of type id should be done * when the runtime type of the value is the same as {@link #defaultImpl()}. - * This is useful because during deserialization, if no type id is present, + * 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 enabled ({@link OptBoolean#TRUE}), the type id will NOT be written + * 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. *

- * Default value is {@link OptBoolean#DEFAULT} (which means {@code FALSE}), + * Default value is {@link OptBoolean#DEFAULT} (which means {@code TRUE}), * preserving backwards-compatible behavior of always writing type id. * * @since 2.22 */ - public OptBoolean skipWriteForDefaultImpl() default OptBoolean.DEFAULT; + public OptBoolean writeTypeIdForDefaultImpl() default OptBoolean.DEFAULT; /* /********************************************************************** @@ -420,7 +420,7 @@ public static class Value /** * @since 2.21 */ - protected final Boolean _skipWriteForDefaultImpl; + protected final Boolean _writeTypeIdForDefaultImpl; /* /********************************************************************** @@ -435,11 +435,11 @@ protected Value(Id idType, As inclusionType, } /** - * @since 2.21 + * @since 2.22 */ protected Value(Id idType, As inclusionType, String propertyName, Class defaultImpl, boolean idVisible, - Boolean requireTypeIdForSubtypes, Boolean skipWriteForDefaultImpl) + Boolean requireTypeIdForSubtypes, Boolean writeTypeIdForDefaultImpl) { _defaultImpl = defaultImpl; _idType = idType; @@ -447,7 +447,7 @@ protected Value(Id idType, As inclusionType, _propertyName = propertyName; _idVisible = idVisible; _requireTypeIdForSubtypes = requireTypeIdForSubtypes; - _skipWriteForDefaultImpl = skipWriteForDefaultImpl; + _writeTypeIdForDefaultImpl = writeTypeIdForDefaultImpl; } /** @@ -455,17 +455,18 @@ protected Value(Id idType, As inclusionType, */ @Deprecated public static Value construct(Id idType, As inclusionType, - String propertyName, Class defaultImpl, boolean idVisible, Boolean requireTypeIdForSubtypes) + String propertyName, Class defaultImpl, boolean idVisible, + Boolean requireTypeIdForSubtypes) { return construct(idType, inclusionType, propertyName, defaultImpl, idVisible, requireTypeIdForSubtypes, null); } /** - * @since 2.21 + * @since 2.22 */ public static Value construct(Id idType, As inclusionType, String propertyName, Class defaultImpl, boolean idVisible, - Boolean requireTypeIdForSubtypes, Boolean skipWriteForDefaultImpl) + Boolean requireTypeIdForSubtypes, Boolean writeTypeIdForDefaultImpl) { // couple of overrides we need to apply here. First: if no propertyName specified, // use Id-specific property name @@ -482,7 +483,7 @@ public static Value construct(Id idType, As inclusionType, defaultImpl = null; } return new Value(idType, inclusionType, propertyName, defaultImpl, idVisible, - requireTypeIdForSubtypes, skipWriteForDefaultImpl); + requireTypeIdForSubtypes, writeTypeIdForDefaultImpl); } public static Value from(JsonTypeInfo src) { @@ -492,7 +493,7 @@ public static Value from(JsonTypeInfo src) { return construct(src.use(), src.include(), src.property(), src.defaultImpl(), src.visible(), src.requireTypeIdForSubtypes().asBoolean(), - src.skipWriteForDefaultImpl().asBoolean()); + src.writeTypeIdForDefaultImpl().asBoolean()); } /* @@ -503,40 +504,47 @@ public static Value from(JsonTypeInfo src) { public Value withDefaultImpl(Class impl) { return (impl == _defaultImpl) ? this : - new Value(_idType, _inclusionType, _propertyName, impl, _idVisible, _requireTypeIdForSubtypes, _skipWriteForDefaultImpl); + 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, _skipWriteForDefaultImpl); + 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, _skipWriteForDefaultImpl); + 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, _skipWriteForDefaultImpl); + 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, _skipWriteForDefaultImpl); + 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, _skipWriteForDefaultImpl); + new Value(_idType, _inclusionType, _propertyName, _defaultImpl, _idVisible, + requireTypeIdForSubtypes, _writeTypeIdForDefaultImpl); } /** - * @since 2.21 + * @since 2.22 */ - public Value withSkipWriteForDefaultImpl(Boolean skipWrite) { - return (_skipWriteForDefaultImpl == skipWrite) ? this : - new Value(_idType, _inclusionType, _propertyName, _defaultImpl, _idVisible, _requireTypeIdForSubtypes, skipWrite); + public Value withSkipWriteForDefaultImpl(Boolean writeTypeIdForDefaultImpl) { + return (_writeTypeIdForDefaultImpl == writeTypeIdForDefaultImpl) ? this : + new Value(_idType, _inclusionType, _propertyName, _defaultImpl, _idVisible, + _requireTypeIdForSubtypes, writeTypeIdForDefaultImpl); } /* @@ -558,9 +566,9 @@ public Class valueFor() { public Boolean getRequireTypeIdForSubtypes() { return _requireTypeIdForSubtypes; } /** - * @since 2.21 + * @since 2.22 */ - public Boolean getSkipWriteForDefaultImpl() { return _skipWriteForDefaultImpl; } + public Boolean getWriteTypeIdForDefaultImpl() { return _writeTypeIdForDefaultImpl; } /** * Static helper method for simple(r) checking of whether there's a Value instance @@ -581,10 +589,10 @@ 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,skipWriteForDefaultImpl=%s)", + + ",requireTypeIdForSubtypes=%s,writeTypeIdForDefaultImpl=%s)", _idType, _inclusionType, _propertyName, ((_defaultImpl == null) ? "NULL" : _defaultImpl.getName()), - _idVisible, _requireTypeIdForSubtypes, _skipWriteForDefaultImpl); + _idVisible, _requireTypeIdForSubtypes, _writeTypeIdForDefaultImpl); } @Override @@ -596,7 +604,7 @@ public int hashCode() { hashCode = 31 * hashCode + (_defaultImpl != null ? _defaultImpl.hashCode() : 0); hashCode = 31 * hashCode + (_requireTypeIdForSubtypes ? 11 : -17); hashCode = 31 * hashCode + (_idVisible ? 11 : -17); - hashCode = 31 * hashCode + (_skipWriteForDefaultImpl != null ? _skipWriteForDefaultImpl.hashCode() : 0); + hashCode = 31 * hashCode + (_writeTypeIdForDefaultImpl != null ? _writeTypeIdForDefaultImpl.hashCode() : 0); return hashCode; } @@ -616,7 +624,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._skipWriteForDefaultImpl, b._skipWriteForDefaultImpl) + && 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); From 05aae11f13c6a1faa0c6b929a6f5d75a337ae9b1 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Tue, 31 Mar 2026 16:56:52 -0700 Subject: [PATCH 3/9] ... --- .../jackson/annotation/JsonTypeInfo.java | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java b/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java index 41f9762a..ea856999 100644 --- a/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java +++ b/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java @@ -418,7 +418,7 @@ public static class Value protected final Boolean _requireTypeIdForSubtypes; /** - * @since 2.21 + * @since 2.22 */ protected final Boolean _writeTypeIdForDefaultImpl; @@ -428,12 +428,6 @@ public static class Value /********************************************************************** */ - 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 */ @@ -451,14 +445,13 @@ protected Value(Id idType, As inclusionType, } /** - * @deprecated Since 2.21 use the 7-argument overload + * @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) + protected Value(Id idType, As inclusionType, + String propertyName, Class defaultImpl, boolean idVisible, Boolean requireTypeIdForSubtypes) { - return construct(idType, inclusionType, propertyName, defaultImpl, idVisible, requireTypeIdForSubtypes, null); + this(idType, inclusionType, propertyName, defaultImpl, idVisible, requireTypeIdForSubtypes, null); } /** @@ -486,6 +479,17 @@ public static Value construct(Id idType, As inclusionType, 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) { if (src == null) { return null; @@ -570,6 +574,13 @@ public Class valueFor() { */ 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. From 3c07adfd6f050d42da8cfae6db84162576b58382 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Tue, 31 Mar 2026 16:59:25 -0700 Subject: [PATCH 4/9] Rename with-method `withWriteTypeIdForDefaultImpl()` --- .../java/com/fasterxml/jackson/annotation/JsonTypeInfo.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java b/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java index ea856999..9459a8c8 100644 --- a/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java +++ b/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java @@ -545,7 +545,7 @@ public Value withRequireTypeIdForSubtypes(Boolean requireTypeIdForSubtypes) { /** * @since 2.22 */ - public Value withSkipWriteForDefaultImpl(Boolean writeTypeIdForDefaultImpl) { + public Value withWriteTypeIdForDefaultImpl(Boolean writeTypeIdForDefaultImpl) { return (_writeTypeIdForDefaultImpl == writeTypeIdForDefaultImpl) ? this : new Value(_idType, _inclusionType, _propertyName, _defaultImpl, _idVisible, _requireTypeIdForSubtypes, writeTypeIdForDefaultImpl); From e142a994c42682003f75243693e500c8c39ef239 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Tue, 31 Mar 2026 17:02:51 -0700 Subject: [PATCH 5/9] Fix an NPE in hashCode() --- .../java/com/fasterxml/jackson/annotation/JsonTypeInfo.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java b/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java index 9459a8c8..41250af0 100644 --- a/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java +++ b/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java @@ -613,9 +613,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 + (_writeTypeIdForDefaultImpl != null ? _writeTypeIdForDefaultImpl.hashCode() : 0); + hashCode = 31 * hashCode + Objects.hashCode(_writeTypeIdForDefaultImpl); return hashCode; } From 44dcf52a66c78ff5d5c257c043b39b75d6f7ba64 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Tue, 31 Mar 2026 17:05:49 -0700 Subject: [PATCH 6/9] Fix test regressions --- .../com/fasterxml/jackson/annotation/JsonTypeInfo.java | 2 +- .../fasterxml/jackson/annotation/JsonTypeInfoTest.java | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java b/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java index 41250af0..c20f9951 100644 --- a/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java +++ b/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java @@ -580,7 +580,7 @@ public Class valueFor() { 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. diff --git a/src/test/java/com/fasterxml/jackson/annotation/JsonTypeInfoTest.java b/src/test/java/com/fasterxml/jackson/annotation/JsonTypeInfoTest.java index 6c8a2bfa..87050667 100644 --- a/src/test/java/com/fasterxml/jackson/annotation/JsonTypeInfoTest.java +++ b/src/test/java/com/fasterxml/jackson/annotation/JsonTypeInfoTest.java @@ -56,8 +56,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 +115,7 @@ 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()); } } From 114ee0863de78703d56fe7b7de846e47bac0a861 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Tue, 31 Mar 2026 17:08:01 -0700 Subject: [PATCH 7/9] Minor reordering --- .../jackson/annotation/JsonTypeInfo.java | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java b/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java index c20f9951..a997514f 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. @@ -392,6 +375,23 @@ public abstract static class None {} */ 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 From cd4e5b3aa40584873e5ec6b577c0ea4ead438227 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Tue, 31 Mar 2026 17:12:42 -0700 Subject: [PATCH 8/9] Add tests --- .../jackson/annotation/JsonTypeInfoTest.java | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/src/test/java/com/fasterxml/jackson/annotation/JsonTypeInfoTest.java b/src/test/java/com/fasterxml/jackson/annotation/JsonTypeInfoTest.java index 87050667..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... @@ -118,4 +127,97 @@ public void testDefaultValueForRequireTypeIdForSubtypes() { 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()); + } } From 38eaf3287eae66b9e26082dfc8b61ee8035ad5f5 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Tue, 31 Mar 2026 17:15:01 -0700 Subject: [PATCH 9/9] JAvadoc add --- .../java/com/fasterxml/jackson/annotation/JsonTypeInfo.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java b/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java index a997514f..0f7a2716 100644 --- a/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java +++ b/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java @@ -368,6 +368,9 @@ public enum As { * 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. *