From 13b1d288505a164f21580d939e287ae2cc22e704 Mon Sep 17 00:00:00 2001 From: "Kim, Joo Hyuk" Date: Wed, 17 May 2023 02:23:42 +0900 Subject: [PATCH 1/3] Add `requireTypeIdForSubtypes` property for `JsonTypeInfo.Value` in Jackson 3.0 (#228) --- .../jackson/annotation/JsonTypeInfo.java | 220 +++++++++++++++--- .../jackson/annotation/JsonTypeInfoTest.java | 103 ++++++++ 2 files changed, 285 insertions(+), 38 deletions(-) create mode 100644 src/test/java/com/fasterxml/jackson/annotation/JsonTypeInfoTest.java diff --git a/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java b/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java index 4be529b3..e1a11f37 100644 --- a/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java +++ b/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java @@ -1,9 +1,7 @@ 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; +import java.lang.annotation.*; +import java.util.Objects; /** * Annotation used for configuring details of if and how type information is @@ -94,7 +92,7 @@ public enum Id { * package name is included that is needed to construct fully-qualified name * given fully-qualified name of the declared supertype; additionally a single * leading dot ('.') must be used to indicate that partial class name is used. - * For example, for supertype "com.foobar.Base", and concrete type + * For example, for supertype "com.foo.Base", and concrete type * "com.foo.Impl", only ".Impl" would be included; and for "com.foo.impl.Impl2" * only ".impl.Impl2" would be included. *
@@ -214,8 +212,6 @@ public enum As { * whereas with {@link JsonTypeId}, output of regular property is suppressed. * This mostly matters with respect to output order; this choice is the only * way to ensure specific placement of type id during serialization. - * - * @since 2.3 but databind only since 2.5. */ EXISTING_PROPERTY ; @@ -228,10 +224,9 @@ public enum As { */ /** - * Specifies kind of type metadata to use when serializing - * type information for instances of annotated type - * and its subtypes; as well as what is expected during - * deserialization. + * Specifies kind of type metadata to use when serializing type information + * for instances of annotated type and its subtypes; as well as what is expected + * during deserialization. */ public Id use(); @@ -277,15 +272,11 @@ public enum As { * */ @@ -300,28 +291,9 @@ public enum As { * Default value is false, meaning that Jackson handles and removes * the type identifier from JSON content that is passed to * JsonDeserializer. - * - * @since 2.0 */ 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 - public abstract static class None {} - /** * Specifies whether the type ID should be strictly required during polymorphic * deserialization of its subtypes. @@ -335,8 +307,180 @@ public abstract static class None {} *

* NOTE: This setting is specific to this type and will always override * the global configuration of {@code MapperFeature.REQUIRE_TYPE_ID_FOR_SUBTYPES}. - * - * @since 2.16 */ public OptBoolean requireTypeIdForSubtypes() default OptBoolean.DEFAULT; + + /* + /********************************************************************** + /* Value class used to enclose information, allow for + /* merging of layered configuration settings. + /********************************************************************** + */ + + public static class Value + implements JacksonAnnotationValue, + java.io.Serializable + { + private static final long serialVersionUID = 1L; + + // should not really be needed usually but make sure defalts to `NONE`; other + // values of less interest + protected final static Value EMPTY = new Value(Id.NONE, As.PROPERTY, null, null, false, null); + + protected final Id _idType; + protected final As _inclusionType; + protected final String _propertyName; + + protected final Class _defaultImpl; + protected final boolean _idVisible; + protected final Boolean _requireTypeIdForSubtypes; + + /* + /********************************************************************** + /* Construction + /********************************************************************** + */ + + protected Value(Id idType, As inclusionType, + String propertyName, Class defaultImpl, boolean idVisible, Boolean requireTypeIdForSubtypes) + { + _defaultImpl = defaultImpl; + _idType = idType; + _inclusionType = inclusionType; + _propertyName = propertyName; + _idVisible = idVisible; + _requireTypeIdForSubtypes = requireTypeIdForSubtypes; + } + + public static Value construct(Id idType, As inclusionType, + String propertyName, Class defaultImpl, boolean idVisible, Boolean requireTypeIdForSubtypes) + { + // couple of overrides we need to apply here. First: if no propertyName specified, + // use Id-specific property name + if ((propertyName == null) || propertyName.isEmpty()) { + if (idType != null) { + propertyName = idType.getDefaultPropertyName(); + } else { + propertyName = ""; + } + } + // Although we can not do much here for special handling of `Void`, we can convert + // annotation types as `null` (== no default implementation) + if ((defaultImpl == null) || defaultImpl.isAnnotation()) { + defaultImpl = null; + } + return new Value(idType, inclusionType, propertyName, defaultImpl, idVisible, requireTypeIdForSubtypes); + } + + public static Value from(JsonTypeInfo src) { + if (src == null) { + return null; + } + return construct(src.use(), src.include(), + src.property(), src.defaultImpl(), src.visible(), src.requireTypeIdForSubtypes().asBoolean()); + } + + /* + /********************************************************************** + /* Mutators + /********************************************************************** + */ + + public Value withDefaultImpl(Class impl) { + return (impl == _defaultImpl) ? this : + new Value(_idType, _inclusionType, _propertyName, impl, _idVisible, _requireTypeIdForSubtypes); + } + + public Value withIdType(Id idType) { + return (idType == _idType) ? this : + new Value(idType, _inclusionType, _propertyName, _defaultImpl, _idVisible, _requireTypeIdForSubtypes); + } + + public Value withInclusionType(As inclusionType) { + return (inclusionType == _inclusionType) ? this : + new Value(_idType, inclusionType, _propertyName, _defaultImpl, _idVisible, _requireTypeIdForSubtypes); + } + + public Value withPropertyName(String propName) { + return (propName == _propertyName) ? this : + new Value(_idType, _inclusionType, propName, _defaultImpl, _idVisible, _requireTypeIdForSubtypes); + } + + public Value withIdVisible(boolean visible) { + return (visible == _idVisible) ? this : + new Value(_idType, _inclusionType, _propertyName, _defaultImpl, visible, _requireTypeIdForSubtypes); + } + + public Value withRequireTypeIdForSubtypes(Boolean requireTypeIdForSubtypes) { + return (_requireTypeIdForSubtypes == requireTypeIdForSubtypes) ? this : + new Value(_idType, _inclusionType, _propertyName, _defaultImpl, _idVisible, requireTypeIdForSubtypes); + } + + /* + /********************************************************************** + /* Simple accessors + /********************************************************************** + */ + + @Override + public Class valueFor() { + return JsonTypeInfo.class; + } + + public Class getDefaultImpl() { return _defaultImpl; } + public Id getIdType() { return _idType; } + public As getInclusionType() { return _inclusionType; } + public String getPropertyName() { return _propertyName; } + public boolean getIdVisible() { return _idVisible; } + public Boolean getRequireTypeIdForSubtypes() { return _requireTypeIdForSubtypes; } + + /** + * Static helper method for simple(r) checking of whether there's a Value instance + * that indicates that polymorphic handling is (to be) enabled. + */ + public static boolean isEnabled(JsonTypeInfo.Value v) { + return (v != null) && + (v._idType != null) && (v._idType != Id.NONE); + } + + /* + /********************************************************************** + /* Standard methods + /********************************************************************** + */ + + @Override + public String toString() { + return String.format("JsonTypeInfo.Value(idType=%s,includeAs=%s,propertyName=%s,defaultImpl=%s,idVisible=%s" + + ",requireTypeIdForSubtypes=%s)", + _idType, _inclusionType, _propertyName, + ((_defaultImpl == null) ? "NULL" : _defaultImpl.getName()), + _idVisible, _requireTypeIdForSubtypes); + } + + @Override + public int hashCode() { + return Objects.hash(_idType, _inclusionType, _propertyName, _defaultImpl, _requireTypeIdForSubtypes) + + (_idVisible ? 11 : -17); + } + + @Override + public boolean equals(Object o) { + if (o == this) return true; + if (o == null) return false; + return (o.getClass() == getClass()) + && _equals(this, (Value) o); + } + + private static boolean _equals(Value a, Value b) + { + return (a._idType == b._idType) + && (a._inclusionType == b._inclusionType) + && (a._defaultImpl == b._defaultImpl) + && (a._idVisible == b._idVisible) + && Objects.equals(a._propertyName, b._propertyName) + && Objects.equals(a._requireTypeIdForSubtypes, b._requireTypeIdForSubtypes) + ; + } + } } diff --git a/src/test/java/com/fasterxml/jackson/annotation/JsonTypeInfoTest.java b/src/test/java/com/fasterxml/jackson/annotation/JsonTypeInfoTest.java new file mode 100644 index 00000000..82b76ccc --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/annotation/JsonTypeInfoTest.java @@ -0,0 +1,103 @@ +package com.fasterxml.jackson.annotation; + +import com.fasterxml.jackson.annotation.JsonTypeInfo.As; + +public class JsonTypeInfoTest extends TestBase +{ + @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, visible=true, + defaultImpl = JsonTypeInfo.class, requireTypeIdForSubtypes = OptBoolean.TRUE) + private final static class Anno1 { } + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = As.EXTERNAL_PROPERTY, + property = "ext", + defaultImpl = Void.class, requireTypeIdForSubtypes = OptBoolean.FALSE) + private final static class Anno2 { } + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = As.EXTERNAL_PROPERTY, + property = "ext", + defaultImpl = Void.class) + private final static class Anno3 { } + + public void testEmpty() { + // 07-Mar-2017, tatu: Important to distinguish "none" from 'empty' value... + assertNull(JsonTypeInfo.Value.from(null)); + } + + public void testFromAnnotation() throws Exception + { + JsonTypeInfo.Value v1 = JsonTypeInfo.Value.from(Anno1.class.getAnnotation(JsonTypeInfo.class)); + assertEquals(JsonTypeInfo.Id.CLASS, v1.getIdType()); + // default from annotation definition + assertEquals(JsonTypeInfo.As.PROPERTY, v1.getInclusionType()); + // default from annotation definition + assertEquals("@class", v1.getPropertyName()); + assertTrue(v1.getIdVisible()); + assertNull(v1.getDefaultImpl()); + assertTrue(v1.getRequireTypeIdForSubtypes()); + + JsonTypeInfo.Value v2 = JsonTypeInfo.Value.from(Anno2.class.getAnnotation(JsonTypeInfo.class)); + assertEquals(JsonTypeInfo.Id.NAME, v2.getIdType()); + assertEquals(JsonTypeInfo.As.EXTERNAL_PROPERTY, v2.getInclusionType()); + assertEquals("ext", v2.getPropertyName()); + assertFalse(v2.getIdVisible()); + assertEquals(Void.class, v2.getDefaultImpl()); + assertFalse(v2.getRequireTypeIdForSubtypes()); + + assertTrue(v1.equals(v1)); + assertTrue(v2.equals(v2)); + + 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()); + } + + public void testMutators() throws Exception + { + JsonTypeInfo.Value v = JsonTypeInfo.Value.from(Anno1.class.getAnnotation(JsonTypeInfo.class)); + assertEquals(JsonTypeInfo.Id.CLASS, v.getIdType()); + + assertSame(v, v.withIdType(JsonTypeInfo.Id.CLASS)); + JsonTypeInfo.Value v2 = v.withIdType(JsonTypeInfo.Id.MINIMAL_CLASS); + assertEquals(JsonTypeInfo.Id.MINIMAL_CLASS, v2.getIdType()); + + assertEquals(JsonTypeInfo.As.PROPERTY, v.getInclusionType()); + assertSame(v, v.withInclusionType(JsonTypeInfo.As.PROPERTY)); + v2 = v.withInclusionType(JsonTypeInfo.As.EXTERNAL_PROPERTY); + assertEquals(JsonTypeInfo.As.EXTERNAL_PROPERTY, v2.getInclusionType()); + + assertSame(v, v.withDefaultImpl(null)); + v2 = v.withDefaultImpl(String.class); + assertEquals(String.class, v2.getDefaultImpl()); + + assertSame(v, v.withIdVisible(true)); + assertFalse(v.withIdVisible(false).getIdVisible()); + + assertEquals("foobar", v.withPropertyName("foobar").getPropertyName()); + } + + public void testWithRequireTypeIdForSubtypes() { + JsonTypeInfo.Value empty = JsonTypeInfo.Value.EMPTY; + assertNull(empty.getRequireTypeIdForSubtypes()); + + JsonTypeInfo.Value requireTypeIdTrue = empty.withRequireTypeIdForSubtypes(Boolean.TRUE); + assertEquals(Boolean.TRUE, requireTypeIdTrue.getRequireTypeIdForSubtypes()); + + JsonTypeInfo.Value requireTypeIdFalse = empty.withRequireTypeIdForSubtypes(Boolean.FALSE); + assertEquals(Boolean.FALSE, requireTypeIdFalse.getRequireTypeIdForSubtypes()); + + JsonTypeInfo.Value requireTypeIdDefault = empty.withRequireTypeIdForSubtypes(null); + assertNull(requireTypeIdDefault.getRequireTypeIdForSubtypes()); + } + + public void testDefaultValueForRequireTypeIdForSubtypes() { + // default value + JsonTypeInfo.Value v3 = JsonTypeInfo.Value.from(Anno3.class.getAnnotation(JsonTypeInfo.class)); + assertNull(v3.getRequireTypeIdForSubtypes()); + + // toString() + assertEquals("JsonTypeInfo.Value(idType=NAME,includeAs=EXTERNAL_PROPERTY,propertyName=ext," + + "defaultImpl=java.lang.Void,idVisible=false,requireTypeIdForSubtypes=null)", v3.toString()); + } +} From 44e46555ca9fa6bb711aaca34e90835159f15ae7 Mon Sep 17 00:00:00 2001 From: joohyukkim Date: Fri, 19 May 2023 00:20:08 +0900 Subject: [PATCH 2/3] Convert JDK 7 equals and hashcode implementation into JDK7. --- .../jackson/annotation/JsonTypeInfo.java | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java b/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java index e1a11f37..159028ab 100644 --- a/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java +++ b/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java @@ -460,8 +460,14 @@ public String toString() { @Override public int hashCode() { - return Objects.hash(_idType, _inclusionType, _propertyName, _defaultImpl, _requireTypeIdForSubtypes) - + (_idVisible ? 11 : -17); + int hashCode = 1; + hashCode = 31 * hashCode + (_idType != null ? _idType.hashCode() : 0); + 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 + (_idVisible ? 11 : -17); + return hashCode; } @Override @@ -478,9 +484,20 @@ private static boolean _equals(Value a, Value b) && (a._inclusionType == b._inclusionType) && (a._defaultImpl == b._defaultImpl) && (a._idVisible == b._idVisible) - && Objects.equals(a._propertyName, b._propertyName) - && Objects.equals(a._requireTypeIdForSubtypes, b._requireTypeIdForSubtypes) + && _equal(a._propertyName, b._propertyName) + && _equal(a._requireTypeIdForSubtypes, b._requireTypeIdForSubtypes) ; } + + private static boolean _equal(T value1, T value2) + { + if (value1 == null) { + return (value2 == null); + } + if (value2 == null) { + return false; + } + return value1.equals(value2); + } } } From 55a726f6ace500f7734ffedaf39029b7ffaa298a Mon Sep 17 00:00:00 2001 From: joohyukkim Date: Fri, 19 May 2023 00:28:10 +0900 Subject: [PATCH 3/3] Remove unnecessary changes --- .../jackson/annotation/JsonTypeInfo.java | 46 +++++++++++++++---- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java b/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java index 159028ab..e16c7414 100644 --- a/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java +++ b/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java @@ -1,7 +1,9 @@ package com.fasterxml.jackson.annotation; -import java.lang.annotation.*; -import java.util.Objects; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; /** * Annotation used for configuring details of if and how type information is @@ -92,7 +94,7 @@ public enum Id { * package name is included that is needed to construct fully-qualified name * given fully-qualified name of the declared supertype; additionally a single * leading dot ('.') must be used to indicate that partial class name is used. - * For example, for supertype "com.foo.Base", and concrete type + * For example, for supertype "com.foobar.Base", and concrete type * "com.foo.Impl", only ".Impl" would be included; and for "com.foo.impl.Impl2" * only ".impl.Impl2" would be included. *
@@ -212,6 +214,8 @@ public enum As { * whereas with {@link JsonTypeId}, output of regular property is suppressed. * This mostly matters with respect to output order; this choice is the only * way to ensure specific placement of type id during serialization. + * + * @since 2.3 but databind only since 2.5. */ EXISTING_PROPERTY ; @@ -224,9 +228,10 @@ public enum As { */ /** - * Specifies kind of type metadata to use when serializing type information - * for instances of annotated type and its subtypes; as well as what is expected - * during deserialization. + * Specifies kind of type metadata to use when serializing + * type information for instances of annotated type + * and its subtypes; as well as what is expected during + * deserialization. */ public Id use(); @@ -272,11 +277,15 @@ public enum As { *

*/ @@ -291,9 +300,28 @@ public enum As { * Default value is false, meaning that Jackson handles and removes * the type identifier from JSON content that is passed to * JsonDeserializer. + * + * @since 2.0 */ 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 + public abstract static class None {} + /** * Specifies whether the type ID should be strictly required during polymorphic * deserialization of its subtypes. @@ -307,6 +335,8 @@ public enum As { *

* NOTE: This setting is specific to this type and will always override * the global configuration of {@code MapperFeature.REQUIRE_TYPE_ID_FOR_SUBTYPES}. + * + * @since 2.16 */ public OptBoolean requireTypeIdForSubtypes() default OptBoolean.DEFAULT;