From 30da2f32e0618fa9414c9cc72ef09caec5b18414 Mon Sep 17 00:00:00 2001 From: black Date: Sat, 11 Nov 2023 14:45:43 +0800 Subject: [PATCH 1/8] feat: add subtype module --- pom.xml | 1 + subtype/README.md | 66 +++++ subtype/pom.xml | 76 +++++ .../jackson/module/subtype/JsonSubType.java | 41 +++ .../module/subtype/PackageVersion.java.in | 20 ++ .../jackson/module/subtype/SubtypeModule.java | 120 ++++++++ subtype/src/main/resources/META-INF/LICENSE | 8 + subtype/src/main/resources/META-INF/NOTICE | 20 ++ .../com.fasterxml.jackson.databind.Module | 1 + subtype/src/moditect/module-info.java | 8 + .../module/subtype/WithJsonSubTypesTest.java | 273 ++++++++++++++++++ .../subtype/WithoutJsonSubTypesTest.java | 121 ++++++++ 12 files changed, 755 insertions(+) create mode 100644 subtype/README.md create mode 100644 subtype/pom.xml create mode 100644 subtype/src/main/java/com/fasterxml/jackson/module/subtype/JsonSubType.java create mode 100644 subtype/src/main/java/com/fasterxml/jackson/module/subtype/PackageVersion.java.in create mode 100644 subtype/src/main/java/com/fasterxml/jackson/module/subtype/SubtypeModule.java create mode 100644 subtype/src/main/resources/META-INF/LICENSE create mode 100644 subtype/src/main/resources/META-INF/NOTICE create mode 100644 subtype/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module create mode 100644 subtype/src/moditect/module-info.java create mode 100644 subtype/src/test/java/com/fasterxml/jackson/module/subtype/WithJsonSubTypesTest.java create mode 100644 subtype/src/test/java/com/fasterxml/jackson/module/subtype/WithoutJsonSubTypesTest.java diff --git a/pom.xml b/pom.xml index 21d2ce330..224af5b3e 100644 --- a/pom.xml +++ b/pom.xml @@ -33,6 +33,7 @@ not datatype, data format, or JAX-RS provider modules. mrbean osgi paranamer + subtype no-ctor-deser diff --git a/subtype/README.md b/subtype/README.md new file mode 100644 index 000000000..10ee00a1a --- /dev/null +++ b/subtype/README.md @@ -0,0 +1,66 @@ +# jackson-module-subtype + +Registering subtypes without annotating the parent class, +see [this](https://github.com/FasterXML/jackson-databind/issues/2104). + +Implementation on SPI. + +# Usage + +Registering modules. + +``` +ObjectMapper mapper = new ObjectMapper().registerModule(new DynamicSubtypeModule()); +``` + +Ensure that the parent class has at least the `JsonTypeInfo` annotation. + +```java +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +public interface Parent { +} +``` + +1. add the `JsonSubType` annotation to your subclass. +2. provide a non-argument constructor (SPI require it). + +```java +import io.github.black.jackson.JsonSubType; + +@JsonSubType("first-child") +public class FirstChild { + + private String foo; + // ... + + public FirstChild() { + } +} +``` + +SPI: Put the subclasses in the `META-INF/services` directory under the interface. +Example: `META-INF/services/package.Parent` + +``` +package.FirstChild +``` + +Alternatively, you can also use the `auto-service` to auto-generate these files: + +```java +import io.github.black.jackson.JsonSubType; +import com.google.auto.service.AutoService; + +@AutoService(Parent.class) +@JsonSubType("first-child") +public class FirstChild { + + private String foo; + // ... + + public FirstChild() { + } +} +``` + +Done, enjoy it. \ No newline at end of file diff --git a/subtype/pom.xml b/subtype/pom.xml new file mode 100644 index 000000000..99c921899 --- /dev/null +++ b/subtype/pom.xml @@ -0,0 +1,76 @@ + + + + + + + + 4.0.0 + + com.fasterxml.jackson.module + jackson-modules-base + 2.16.0-SNAPSHOT + + jackson-module-subtype + Jackson module: Subtype Annotation Support + bundle + + Registering subtypes without annotating the parent class + https://github.com/FasterXML/jackson-modules-base + + + + The Apache Software License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + com/fasterxml/jackson/module/subtype + com.fasterxml.jackson.module.subtype + + + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-databind + + + com.google.auto.service + auto-service + 1.0.1 + test + + + + + + + com.google.code.maven-replacer-plugin + replacer + + + + org.moditect + moditect-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + + 9 + 9 + + + + + \ No newline at end of file diff --git a/subtype/src/main/java/com/fasterxml/jackson/module/subtype/JsonSubType.java b/subtype/src/main/java/com/fasterxml/jackson/module/subtype/JsonSubType.java new file mode 100644 index 000000000..4d499fada --- /dev/null +++ b/subtype/src/main/java/com/fasterxml/jackson/module/subtype/JsonSubType.java @@ -0,0 +1,41 @@ +package com.fasterxml.jackson.module.subtype; + +import com.fasterxml.jackson.annotation.JacksonAnnotation; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeName; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Definition of a subtype, along with optional name(s). If no name is defined + * (empty Strings are ignored), class of the type will be checked for {@link JsonTypeName} + * annotation; and if that is also missing or empty, a default + * name will be constructed by type id mechanism. + * Default name is usually based on class name. + *

+ * It's the same as {@link JsonSubTypes.Type}. + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@JacksonAnnotation +public @interface JsonSubType { + /** + * Logical type name used as the type identifier for the class, if defined; empty + * String means "not defined". Used unless {@link #names} is defined as non-empty. + * + * @return subtype name + */ + String value() default ""; + + /** + * (optional) Logical type names used as the type identifier for the class: used if + * more than one type name should be associated with the same type. + * + * @return subtype name array + * @since 2.12 + */ + String[] names() default {}; +} diff --git a/subtype/src/main/java/com/fasterxml/jackson/module/subtype/PackageVersion.java.in b/subtype/src/main/java/com/fasterxml/jackson/module/subtype/PackageVersion.java.in new file mode 100644 index 000000000..7860aa14b --- /dev/null +++ b/subtype/src/main/java/com/fasterxml/jackson/module/subtype/PackageVersion.java.in @@ -0,0 +1,20 @@ +package @package@; + +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.core.Versioned; +import com.fasterxml.jackson.core.util.VersionUtil; + +/** + * Automatically generated from PackageVersion.java.in during + * packageVersion-generate execution of maven-replacer-plugin in + * pom.xml. + */ +public final class PackageVersion implements Versioned { + public final static Version VERSION = VersionUtil.parseVersion( + "@projectversion@", "@projectgroupid@", "@projectartifactid@"); + + @Override + public Version version() { + return VERSION; + } +} diff --git a/subtype/src/main/java/com/fasterxml/jackson/module/subtype/SubtypeModule.java b/subtype/src/main/java/com/fasterxml/jackson/module/subtype/SubtypeModule.java new file mode 100644 index 000000000..e1f7ab4a4 --- /dev/null +++ b/subtype/src/main/java/com/fasterxml/jackson/module/subtype/SubtypeModule.java @@ -0,0 +1,120 @@ +package com.fasterxml.jackson.module.subtype; + +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.databind.AnnotationIntrospector; +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.introspect.Annotated; +import com.fasterxml.jackson.databind.jsontype.NamedType; +import com.fasterxml.jackson.module.subtype.PackageVersion; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.ServiceLoader; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +/** + * Subtype module. + *

+ * The module caches the subclass, so it's non-real-time. + * It's for registering subtypes without annotating the parent class. + * See this issues in jackson-databind. + *

+ * When not found in the cache, it loads and caches subclasses using SPI. + * Therefore, we can {@link #unregisterType} a class and then module will reload this class's subclasses. + */ +public class SubtypeModule extends Module { + + private final ConcurrentHashMap, List> subtypes = new ConcurrentHashMap<>(); + + @Override + public String getModuleName() { + return getClass().getSimpleName(); + } + + @Override + public Version version() { + return PackageVersion.VERSION; + } + + @Override + public void setupModule(SetupContext context) { + context.insertAnnotationIntrospector(new AnnotationIntrospector() { + @Override + public Version version() { + return PackageVersion.VERSION; + } + + @Override + public List findSubtypes(Annotated a) { + registerTypes(a.getRawType()); + + List list1 = SubtypeModule.findSubtypes(a.getRawType(), a::getAnnotation); + List list2 = subtypes.getOrDefault(a.getRawType(), Collections.emptyList()); + + if (list1.isEmpty()) return list2; + if (list2.isEmpty()) return list1; + List list = new ArrayList<>(list1.size() + list2.size()); + list.addAll(list1); + list.addAll(list2); + return list; + } + }); + } + + /** + * load parent's subclass by SPI. + * + * @param parent parent class. + * @param parent class type. + */ + @SuppressWarnings("unchecked") + public void registerTypes(Class parent) { + if (subtypes.containsKey(parent)) { + return; + } + List> subclasses = new ArrayList<>(); + for (S instance : ServiceLoader.load(parent)) { + subclasses.add((Class) instance.getClass()); + } + this.registerTypes(parent, subclasses); + } + + /** + * register subtypes without SPI. + * Of course, you need to provide them :) + * + * @param parent: parent class. + * @param subclasses: children class. + * @param : parent class type. + */ + public void registerTypes(Class parent, Iterable> subclasses) { + List result = new ArrayList<>(); + for (Class subclass : subclasses) { + result.addAll(findSubtypes(subclass, subclass::getAnnotation)); + } + subtypes.put(parent, result); + } + + public void unregisterType(Class parent) { + subtypes.remove(parent); + } + + private static List findSubtypes(Class clazz, Function, JsonSubType> getter) { + if (clazz == null) { + return Collections.emptyList(); + } + JsonSubType subtype = getter.apply(JsonSubType.class); + if (subtype == null) { + return Collections.emptyList(); + } + List result = new ArrayList<>(); + result.add(new NamedType(clazz, subtype.value())); + // [databind#2761]: alternative set of names to use + for (String name : subtype.names()) { + result.add(new NamedType(clazz, name)); + } + return result; + } +} diff --git a/subtype/src/main/resources/META-INF/LICENSE b/subtype/src/main/resources/META-INF/LICENSE new file mode 100644 index 000000000..a9e546210 --- /dev/null +++ b/subtype/src/main/resources/META-INF/LICENSE @@ -0,0 +1,8 @@ +This copy of Jackson JSON processor `jackson-module-guice` module is licensed under the +Apache (Software) License, version 2.0 ("the License"). +See the License for details about distribution rights, and the +specific rights regarding derivative works. + +You may obtain a copy of the License at: + +http://www.apache.org/licenses/LICENSE-2.0 diff --git a/subtype/src/main/resources/META-INF/NOTICE b/subtype/src/main/resources/META-INF/NOTICE new file mode 100644 index 000000000..4c976b7b4 --- /dev/null +++ b/subtype/src/main/resources/META-INF/NOTICE @@ -0,0 +1,20 @@ +# Jackson JSON processor + +Jackson is a high-performance, Free/Open Source JSON processing library. +It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has +been in development since 2007. +It is currently developed by a community of developers, as well as supported +commercially by FasterXML.com. + +## Licensing + +Jackson core and extension components may licensed under different licenses. +To find the details that apply to this artifact see the accompanying LICENSE file. +For more information, including possible other licensing options, contact +FasterXML.com (http://fasterxml.com). + +## Credits + +A list of contributors may be found from CREDITS file, which is included +in some artifacts (usually source distributions); but is always available +from the source code management (SCM) system project uses. diff --git a/subtype/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module b/subtype/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module new file mode 100644 index 000000000..f0d3925eb --- /dev/null +++ b/subtype/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module @@ -0,0 +1 @@ +com.fasterxml.jackson.module.subtype.SubtypeModule \ No newline at end of file diff --git a/subtype/src/moditect/module-info.java b/subtype/src/moditect/module-info.java new file mode 100644 index 000000000..4988481f0 --- /dev/null +++ b/subtype/src/moditect/module-info.java @@ -0,0 +1,8 @@ +module com.fasterxml.jackson.module.subtype { + + requires com.fasterxml.jackson.core; + requires com.fasterxml.jackson.annotation; + requires com.fasterxml.jackson.databind; + + exports com.fasterxml.jackson.module.subtype; +} diff --git a/subtype/src/test/java/com/fasterxml/jackson/module/subtype/WithJsonSubTypesTest.java b/subtype/src/test/java/com/fasterxml/jackson/module/subtype/WithJsonSubTypesTest.java new file mode 100644 index 000000000..a7bf70a7a --- /dev/null +++ b/subtype/src/test/java/com/fasterxml/jackson/module/subtype/WithJsonSubTypesTest.java @@ -0,0 +1,273 @@ +package com.fasterxml.jackson.module.subtype; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.auto.service.AutoService; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +import static org.junit.Assert.*; + +/** + * test work with {@link JsonSubTypes} + */ +@RunWith(value = Parameterized.class) +public class WithJsonSubTypesTest { + + private final ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + + public static class Argument { + private final Class clazz; + private final T expected; + + public Argument(Class clazz, T expected) { + this.clazz = clazz; + this.expected = expected; + } + } + + @Parameter + public Argument argument; + + @Parameters + public static Collection> data() { + return Arrays.asList( + new Argument<>(FirstChild.class, new FirstChild("hello")), + new Argument<>(SecondChild.class, new SecondChild("world")), + new Argument<>(FirstAppendChild.class, new FirstAppendChild(42)), + new Argument<>(SecondAppendChild.class, new SecondAppendChild("42", Arrays.asList("hello", "foo", "bar"))), + new Argument<>(ThirdAppendChild.class, new ThirdAppendChild("42", Arrays.asList("hello", "foo", "bar"), 3.1415926)) + ); + } + + @Test + public void test() throws Exception { + final Parent parent = argument.expected; + String json = mapper.writeValueAsString(parent); + Parent unmarshal = mapper.readValue(json, Parent.class); + T actual = assertInstanceOf(argument.clazz, unmarshal); + assertEquals(argument.expected, actual); + } + + public static T assertInstanceOf(Class expectedType, Object actualValue) { + assertTrue(expectedType.isInstance(actualValue)); + return expectedType.cast(actualValue); + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") + @JsonSubTypes(value = { + @JsonSubTypes.Type(value = FirstChild.class, name = "first-child"), + @JsonSubTypes.Type(value = SecondChild.class, name = "second-child"), + }) + public interface Parent { + } + + public static class FirstChild implements Parent { + private String foo; + + @SuppressWarnings("unused") // SPI require it + public FirstChild() { + } + + public FirstChild(String foo) { + this.foo = foo; + } + + @SuppressWarnings("unused") // jackson require it + public String getFoo() { + return foo; + } + + @SuppressWarnings("unused") // jackson require it + public void setFoo(String foo) { + this.foo = foo; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + FirstChild that = (FirstChild) o; + + return Objects.equals(foo, that.foo); + } + + @Override + public int hashCode() { + return foo != null ? foo.hashCode() : 0; + } + } + + public static class SecondChild implements Parent { + private String bar; + + public SecondChild() { + } + + public SecondChild(String bar) { + this.bar = bar; + } + + @SuppressWarnings("unused") // jackson require it + public String getBar() { + return bar; + } + + @SuppressWarnings("unused") // jackson require it + public void setBar(String bar) { + this.bar = bar; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + SecondChild that = (SecondChild) o; + + return Objects.equals(bar, that.bar); + } + + @Override + public int hashCode() { + return bar != null ? bar.hashCode() : 0; + } + } + + @JsonSubType("first-append-child") + @AutoService(Parent.class) + public static class FirstAppendChild implements Parent { + private Integer integer; + + @SuppressWarnings("unused") // SPI require it + public FirstAppendChild() { + } + + public FirstAppendChild(Integer integer) { + this.integer = integer; + } + + @SuppressWarnings("unused") // jackson require it + public Integer getInteger() { + return integer; + } + + @SuppressWarnings("unused") // jackson require it + public void setInteger(Integer integer) { + this.integer = integer; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + FirstAppendChild that = (FirstAppendChild) o; + + return Objects.equals(integer, that.integer); + } + + @Override + public int hashCode() { + return integer != null ? integer.hashCode() : 0; + } + } + + + @JsonSubType("second-append-child") + @AutoService(Parent.class) + public static class SecondAppendChild extends SecondChild { + private List list; + + public SecondAppendChild() { + } + + public SecondAppendChild(String bar, List list) { + super(bar); + this.list = list; + } + + @SuppressWarnings("unused") // jackson require it + public List getList() { + return list; + } + + @SuppressWarnings("unused") // jackson require it + public void setList(List list) { + this.list = list; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + + SecondAppendChild that = (SecondAppendChild) o; + + return Objects.equals(list, that.list); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + (list != null ? list.hashCode() : 0); + return result; + } + } + + @JsonSubType("third-append-child") + @AutoService(Parent.class) + public static class ThirdAppendChild extends SecondAppendChild { + private double value; + + @SuppressWarnings("unused") // SPI require it + public ThirdAppendChild() { + } + + public ThirdAppendChild(String bar, List list, double value) { + super(bar, list); + this.value = value; + } + + @SuppressWarnings("unused") // jackson require it + public double getValue() { + return value; + } + + @SuppressWarnings("unused") // jackson require it + public void setValue(double value) { + this.value = value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + + ThirdAppendChild that = (ThirdAppendChild) o; + + return Double.compare(value, that.value) == 0; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + long temp; + temp = Double.doubleToLongBits(value); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + return result; + } + } +} \ No newline at end of file diff --git a/subtype/src/test/java/com/fasterxml/jackson/module/subtype/WithoutJsonSubTypesTest.java b/subtype/src/test/java/com/fasterxml/jackson/module/subtype/WithoutJsonSubTypesTest.java new file mode 100644 index 000000000..909754f6d --- /dev/null +++ b/subtype/src/test/java/com/fasterxml/jackson/module/subtype/WithoutJsonSubTypesTest.java @@ -0,0 +1,121 @@ +package com.fasterxml.jackson.module.subtype; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.auto.service.AutoService; +import org.junit.Test; + +import java.util.Objects; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * test work without {@link JsonSubTypes} + */ +public class WithoutJsonSubTypesTest { + private final ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + + @Test + public void testFirstChild() throws Exception { + FirstChild child = new FirstChild(); + child.setFoo("hello"); + String json = mapper.writeValueAsString(child); + + // {"type":"first-child","foo":"hello"} + + Parent unmarshal = mapper.readValue(json, Parent.class); + FirstChild actual = assertInstanceOf(FirstChild.class, unmarshal); + assertEquals("hello", actual.getFoo()); + } + + @Test + public void testSecondChild() throws Exception { + SecondChild child = new SecondChild(); + child.setBar("world"); + String json = mapper.writeValueAsString(child); + + // {"type":"second-child","bar":"world"} + + Parent unmarshal = mapper.readValue(json, Parent.class); + SecondChild actual = assertInstanceOf(SecondChild.class, unmarshal); + assertEquals("world", actual.getBar()); + } + + public static T assertInstanceOf(Class expectedType, Object actualValue) { + assertTrue(expectedType.isInstance(actualValue)); + return expectedType.cast(actualValue); + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") + public interface Parent { + } + + @JsonSubType("first-child") + @AutoService(Parent.class) // module requires spi + public static class FirstChild implements Parent { + private String foo; + + public FirstChild() { + } + + public String getFoo() { + return foo; + } + + public void setFoo(String foo) { + this.foo = foo; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + FirstChild that = (FirstChild) o; + + return Objects.equals(foo, that.foo); + } + + @Override + public int hashCode() { + return foo != null ? foo.hashCode() : 0; + } + } + + + @JsonSubType("second-child") + @AutoService(Parent.class) // module requires spi + public static class SecondChild implements Parent { + private String bar; + + public SecondChild() { + } + + public String getBar() { + return bar; + } + + public void setBar(String bar) { + this.bar = bar; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + SecondChild that = (SecondChild) o; + + return Objects.equals(bar, that.bar); + } + + @Override + public int hashCode() { + return bar != null ? bar.hashCode() : 0; + } + } + + +} \ No newline at end of file From 5152c26c71374aefc7d85a1d353e49c90ac578f0 Mon Sep 17 00:00:00 2001 From: black Date: Mon, 13 Nov 2023 09:53:24 +0800 Subject: [PATCH 2/8] fix --- subtype/README.md | 4 +- .../jackson/module/subtype/JsonSubType.java | 1 - .../jackson/module/subtype/SubtypeModule.java | 8 ++- .../module/subtype/WithJsonSubTypesTest.java | 64 ++----------------- .../subtype/WithoutJsonSubTypesTest.java | 32 +++------- 5 files changed, 22 insertions(+), 87 deletions(-) diff --git a/subtype/README.md b/subtype/README.md index 10ee00a1a..b23475c64 100644 --- a/subtype/README.md +++ b/subtype/README.md @@ -10,7 +10,7 @@ Implementation on SPI. Registering modules. ``` -ObjectMapper mapper = new ObjectMapper().registerModule(new DynamicSubtypeModule()); +ObjectMapper mapper = new ObjectMapper().registerModule(new SubtypeModule()); ``` Ensure that the parent class has at least the `JsonTypeInfo` annotation. @@ -25,7 +25,7 @@ public interface Parent { 2. provide a non-argument constructor (SPI require it). ```java -import io.github.black.jackson.JsonSubType; +import com.fasterxml.jackson.module.subtype.JsonSubType; @JsonSubType("first-child") public class FirstChild { diff --git a/subtype/src/main/java/com/fasterxml/jackson/module/subtype/JsonSubType.java b/subtype/src/main/java/com/fasterxml/jackson/module/subtype/JsonSubType.java index 4d499fada..842d17bb0 100644 --- a/subtype/src/main/java/com/fasterxml/jackson/module/subtype/JsonSubType.java +++ b/subtype/src/main/java/com/fasterxml/jackson/module/subtype/JsonSubType.java @@ -35,7 +35,6 @@ * more than one type name should be associated with the same type. * * @return subtype name array - * @since 2.12 */ String[] names() default {}; } diff --git a/subtype/src/main/java/com/fasterxml/jackson/module/subtype/SubtypeModule.java b/subtype/src/main/java/com/fasterxml/jackson/module/subtype/SubtypeModule.java index e1f7ab4a4..9b18605c2 100644 --- a/subtype/src/main/java/com/fasterxml/jackson/module/subtype/SubtypeModule.java +++ b/subtype/src/main/java/com/fasterxml/jackson/module/subtype/SubtypeModule.java @@ -23,6 +23,8 @@ *

* When not found in the cache, it loads and caches subclasses using SPI. * Therefore, we can {@link #unregisterType} a class and then module will reload this class's subclasses. + * + * @since 2.16 */ public class SubtypeModule extends Module { @@ -50,7 +52,7 @@ public Version version() { public List findSubtypes(Annotated a) { registerTypes(a.getRawType()); - List list1 = SubtypeModule.findSubtypes(a.getRawType(), a::getAnnotation); + List list1 = _findSubtypes(a.getRawType(), a::getAnnotation); List list2 = subtypes.getOrDefault(a.getRawType(), Collections.emptyList()); if (list1.isEmpty()) return list2; @@ -92,7 +94,7 @@ public void registerTypes(Class parent) { public void registerTypes(Class parent, Iterable> subclasses) { List result = new ArrayList<>(); for (Class subclass : subclasses) { - result.addAll(findSubtypes(subclass, subclass::getAnnotation)); + result.addAll(_findSubtypes(subclass, subclass::getAnnotation)); } subtypes.put(parent, result); } @@ -101,7 +103,7 @@ public void unregisterType(Class parent) { subtypes.remove(parent); } - private static List findSubtypes(Class clazz, Function, JsonSubType> getter) { + private List _findSubtypes(Class clazz, Function, JsonSubType> getter) { if (clazz == null) { return Collections.emptyList(); } diff --git a/subtype/src/test/java/com/fasterxml/jackson/module/subtype/WithJsonSubTypesTest.java b/subtype/src/test/java/com/fasterxml/jackson/module/subtype/WithJsonSubTypesTest.java index a7bf70a7a..eb3e665de 100644 --- a/subtype/src/test/java/com/fasterxml/jackson/module/subtype/WithJsonSubTypesTest.java +++ b/subtype/src/test/java/com/fasterxml/jackson/module/subtype/WithJsonSubTypesTest.java @@ -18,12 +18,12 @@ import static org.junit.Assert.*; /** - * test work with {@link JsonSubTypes} + * test {@link JsonSubType} work with {@link JsonSubTypes} */ @RunWith(value = Parameterized.class) public class WithJsonSubTypesTest { - private final ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + private final ObjectMapper mapper = new ObjectMapper().registerModule(new SubtypeModule()); public static class Argument { private final Class clazz; @@ -72,7 +72,7 @@ public interface Parent { } public static class FirstChild implements Parent { - private String foo; + public String foo; @SuppressWarnings("unused") // SPI require it public FirstChild() { @@ -82,16 +82,6 @@ public FirstChild(String foo) { this.foo = foo; } - @SuppressWarnings("unused") // jackson require it - public String getFoo() { - return foo; - } - - @SuppressWarnings("unused") // jackson require it - public void setFoo(String foo) { - this.foo = foo; - } - @Override public boolean equals(Object o) { if (this == o) return true; @@ -109,7 +99,7 @@ public int hashCode() { } public static class SecondChild implements Parent { - private String bar; + public String bar; public SecondChild() { } @@ -118,16 +108,6 @@ public SecondChild(String bar) { this.bar = bar; } - @SuppressWarnings("unused") // jackson require it - public String getBar() { - return bar; - } - - @SuppressWarnings("unused") // jackson require it - public void setBar(String bar) { - this.bar = bar; - } - @Override public boolean equals(Object o) { if (this == o) return true; @@ -147,7 +127,7 @@ public int hashCode() { @JsonSubType("first-append-child") @AutoService(Parent.class) public static class FirstAppendChild implements Parent { - private Integer integer; + public Integer integer; @SuppressWarnings("unused") // SPI require it public FirstAppendChild() { @@ -157,16 +137,6 @@ public FirstAppendChild(Integer integer) { this.integer = integer; } - @SuppressWarnings("unused") // jackson require it - public Integer getInteger() { - return integer; - } - - @SuppressWarnings("unused") // jackson require it - public void setInteger(Integer integer) { - this.integer = integer; - } - @Override public boolean equals(Object o) { if (this == o) return true; @@ -187,7 +157,7 @@ public int hashCode() { @JsonSubType("second-append-child") @AutoService(Parent.class) public static class SecondAppendChild extends SecondChild { - private List list; + public List list; public SecondAppendChild() { } @@ -197,16 +167,6 @@ public SecondAppendChild(String bar, List list) { this.list = list; } - @SuppressWarnings("unused") // jackson require it - public List getList() { - return list; - } - - @SuppressWarnings("unused") // jackson require it - public void setList(List list) { - this.list = list; - } - @Override public boolean equals(Object o) { if (this == o) return true; @@ -229,7 +189,7 @@ public int hashCode() { @JsonSubType("third-append-child") @AutoService(Parent.class) public static class ThirdAppendChild extends SecondAppendChild { - private double value; + public double value; @SuppressWarnings("unused") // SPI require it public ThirdAppendChild() { @@ -240,16 +200,6 @@ public ThirdAppendChild(String bar, List list, double value) { this.value = value; } - @SuppressWarnings("unused") // jackson require it - public double getValue() { - return value; - } - - @SuppressWarnings("unused") // jackson require it - public void setValue(double value) { - this.value = value; - } - @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/subtype/src/test/java/com/fasterxml/jackson/module/subtype/WithoutJsonSubTypesTest.java b/subtype/src/test/java/com/fasterxml/jackson/module/subtype/WithoutJsonSubTypesTest.java index 909754f6d..77056f9aa 100644 --- a/subtype/src/test/java/com/fasterxml/jackson/module/subtype/WithoutJsonSubTypesTest.java +++ b/subtype/src/test/java/com/fasterxml/jackson/module/subtype/WithoutJsonSubTypesTest.java @@ -12,35 +12,35 @@ import static org.junit.Assert.assertTrue; /** - * test work without {@link JsonSubTypes} + * test {@link JsonSubType} works alone, without {@link JsonSubTypes} */ public class WithoutJsonSubTypesTest { - private final ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + private final ObjectMapper mapper = new ObjectMapper().registerModule(new SubtypeModule()); @Test public void testFirstChild() throws Exception { FirstChild child = new FirstChild(); - child.setFoo("hello"); + child.foo = "hello"; String json = mapper.writeValueAsString(child); // {"type":"first-child","foo":"hello"} Parent unmarshal = mapper.readValue(json, Parent.class); FirstChild actual = assertInstanceOf(FirstChild.class, unmarshal); - assertEquals("hello", actual.getFoo()); + assertEquals("hello", actual.foo); } @Test public void testSecondChild() throws Exception { SecondChild child = new SecondChild(); - child.setBar("world"); + child.bar = "world"; String json = mapper.writeValueAsString(child); // {"type":"second-child","bar":"world"} Parent unmarshal = mapper.readValue(json, Parent.class); SecondChild actual = assertInstanceOf(SecondChild.class, unmarshal); - assertEquals("world", actual.getBar()); + assertEquals("world", actual.bar); } public static T assertInstanceOf(Class expectedType, Object actualValue) { @@ -55,19 +55,11 @@ public interface Parent { @JsonSubType("first-child") @AutoService(Parent.class) // module requires spi public static class FirstChild implements Parent { - private String foo; + public String foo; public FirstChild() { } - public String getFoo() { - return foo; - } - - public void setFoo(String foo) { - this.foo = foo; - } - @Override public boolean equals(Object o) { if (this == o) return true; @@ -88,19 +80,11 @@ public int hashCode() { @JsonSubType("second-child") @AutoService(Parent.class) // module requires spi public static class SecondChild implements Parent { - private String bar; + public String bar; public SecondChild() { } - public String getBar() { - return bar; - } - - public void setBar(String bar) { - this.bar = bar; - } - @Override public boolean equals(Object o) { if (this == o) return true; From 57009d8d6bc3a7117b7406fc480866b7c4ac68bc Mon Sep 17 00:00:00 2001 From: black Date: Sat, 11 Nov 2023 14:45:43 +0800 Subject: [PATCH 3/8] feat: add subtype module --- pom.xml | 1 + subtype/README.md | 66 +++++ subtype/pom.xml | 76 +++++ .../jackson/module/subtype/JsonSubType.java | 41 +++ .../module/subtype/PackageVersion.java.in | 20 ++ .../jackson/module/subtype/SubtypeModule.java | 120 ++++++++ subtype/src/main/resources/META-INF/LICENSE | 8 + subtype/src/main/resources/META-INF/NOTICE | 20 ++ .../com.fasterxml.jackson.databind.Module | 1 + subtype/src/moditect/module-info.java | 8 + .../module/subtype/WithJsonSubTypesTest.java | 273 ++++++++++++++++++ .../subtype/WithoutJsonSubTypesTest.java | 121 ++++++++ 12 files changed, 755 insertions(+) create mode 100644 subtype/README.md create mode 100644 subtype/pom.xml create mode 100644 subtype/src/main/java/com/fasterxml/jackson/module/subtype/JsonSubType.java create mode 100644 subtype/src/main/java/com/fasterxml/jackson/module/subtype/PackageVersion.java.in create mode 100644 subtype/src/main/java/com/fasterxml/jackson/module/subtype/SubtypeModule.java create mode 100644 subtype/src/main/resources/META-INF/LICENSE create mode 100644 subtype/src/main/resources/META-INF/NOTICE create mode 100644 subtype/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module create mode 100644 subtype/src/moditect/module-info.java create mode 100644 subtype/src/test/java/com/fasterxml/jackson/module/subtype/WithJsonSubTypesTest.java create mode 100644 subtype/src/test/java/com/fasterxml/jackson/module/subtype/WithoutJsonSubTypesTest.java diff --git a/pom.xml b/pom.xml index ff0ef8659..ad886a64e 100644 --- a/pom.xml +++ b/pom.xml @@ -33,6 +33,7 @@ not datatype, data format, or JAX-RS provider modules. mrbean osgi paranamer + subtype no-ctor-deser diff --git a/subtype/README.md b/subtype/README.md new file mode 100644 index 000000000..10ee00a1a --- /dev/null +++ b/subtype/README.md @@ -0,0 +1,66 @@ +# jackson-module-subtype + +Registering subtypes without annotating the parent class, +see [this](https://github.com/FasterXML/jackson-databind/issues/2104). + +Implementation on SPI. + +# Usage + +Registering modules. + +``` +ObjectMapper mapper = new ObjectMapper().registerModule(new DynamicSubtypeModule()); +``` + +Ensure that the parent class has at least the `JsonTypeInfo` annotation. + +```java +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +public interface Parent { +} +``` + +1. add the `JsonSubType` annotation to your subclass. +2. provide a non-argument constructor (SPI require it). + +```java +import io.github.black.jackson.JsonSubType; + +@JsonSubType("first-child") +public class FirstChild { + + private String foo; + // ... + + public FirstChild() { + } +} +``` + +SPI: Put the subclasses in the `META-INF/services` directory under the interface. +Example: `META-INF/services/package.Parent` + +``` +package.FirstChild +``` + +Alternatively, you can also use the `auto-service` to auto-generate these files: + +```java +import io.github.black.jackson.JsonSubType; +import com.google.auto.service.AutoService; + +@AutoService(Parent.class) +@JsonSubType("first-child") +public class FirstChild { + + private String foo; + // ... + + public FirstChild() { + } +} +``` + +Done, enjoy it. \ No newline at end of file diff --git a/subtype/pom.xml b/subtype/pom.xml new file mode 100644 index 000000000..99c921899 --- /dev/null +++ b/subtype/pom.xml @@ -0,0 +1,76 @@ + + + + + + + + 4.0.0 + + com.fasterxml.jackson.module + jackson-modules-base + 2.16.0-SNAPSHOT + + jackson-module-subtype + Jackson module: Subtype Annotation Support + bundle + + Registering subtypes without annotating the parent class + https://github.com/FasterXML/jackson-modules-base + + + + The Apache Software License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + com/fasterxml/jackson/module/subtype + com.fasterxml.jackson.module.subtype + + + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-databind + + + com.google.auto.service + auto-service + 1.0.1 + test + + + + + + + com.google.code.maven-replacer-plugin + replacer + + + + org.moditect + moditect-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + + 9 + 9 + + + + + \ No newline at end of file diff --git a/subtype/src/main/java/com/fasterxml/jackson/module/subtype/JsonSubType.java b/subtype/src/main/java/com/fasterxml/jackson/module/subtype/JsonSubType.java new file mode 100644 index 000000000..4d499fada --- /dev/null +++ b/subtype/src/main/java/com/fasterxml/jackson/module/subtype/JsonSubType.java @@ -0,0 +1,41 @@ +package com.fasterxml.jackson.module.subtype; + +import com.fasterxml.jackson.annotation.JacksonAnnotation; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeName; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Definition of a subtype, along with optional name(s). If no name is defined + * (empty Strings are ignored), class of the type will be checked for {@link JsonTypeName} + * annotation; and if that is also missing or empty, a default + * name will be constructed by type id mechanism. + * Default name is usually based on class name. + *

+ * It's the same as {@link JsonSubTypes.Type}. + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@JacksonAnnotation +public @interface JsonSubType { + /** + * Logical type name used as the type identifier for the class, if defined; empty + * String means "not defined". Used unless {@link #names} is defined as non-empty. + * + * @return subtype name + */ + String value() default ""; + + /** + * (optional) Logical type names used as the type identifier for the class: used if + * more than one type name should be associated with the same type. + * + * @return subtype name array + * @since 2.12 + */ + String[] names() default {}; +} diff --git a/subtype/src/main/java/com/fasterxml/jackson/module/subtype/PackageVersion.java.in b/subtype/src/main/java/com/fasterxml/jackson/module/subtype/PackageVersion.java.in new file mode 100644 index 000000000..7860aa14b --- /dev/null +++ b/subtype/src/main/java/com/fasterxml/jackson/module/subtype/PackageVersion.java.in @@ -0,0 +1,20 @@ +package @package@; + +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.core.Versioned; +import com.fasterxml.jackson.core.util.VersionUtil; + +/** + * Automatically generated from PackageVersion.java.in during + * packageVersion-generate execution of maven-replacer-plugin in + * pom.xml. + */ +public final class PackageVersion implements Versioned { + public final static Version VERSION = VersionUtil.parseVersion( + "@projectversion@", "@projectgroupid@", "@projectartifactid@"); + + @Override + public Version version() { + return VERSION; + } +} diff --git a/subtype/src/main/java/com/fasterxml/jackson/module/subtype/SubtypeModule.java b/subtype/src/main/java/com/fasterxml/jackson/module/subtype/SubtypeModule.java new file mode 100644 index 000000000..e1f7ab4a4 --- /dev/null +++ b/subtype/src/main/java/com/fasterxml/jackson/module/subtype/SubtypeModule.java @@ -0,0 +1,120 @@ +package com.fasterxml.jackson.module.subtype; + +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.databind.AnnotationIntrospector; +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.introspect.Annotated; +import com.fasterxml.jackson.databind.jsontype.NamedType; +import com.fasterxml.jackson.module.subtype.PackageVersion; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.ServiceLoader; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +/** + * Subtype module. + *

+ * The module caches the subclass, so it's non-real-time. + * It's for registering subtypes without annotating the parent class. + * See this issues in jackson-databind. + *

+ * When not found in the cache, it loads and caches subclasses using SPI. + * Therefore, we can {@link #unregisterType} a class and then module will reload this class's subclasses. + */ +public class SubtypeModule extends Module { + + private final ConcurrentHashMap, List> subtypes = new ConcurrentHashMap<>(); + + @Override + public String getModuleName() { + return getClass().getSimpleName(); + } + + @Override + public Version version() { + return PackageVersion.VERSION; + } + + @Override + public void setupModule(SetupContext context) { + context.insertAnnotationIntrospector(new AnnotationIntrospector() { + @Override + public Version version() { + return PackageVersion.VERSION; + } + + @Override + public List findSubtypes(Annotated a) { + registerTypes(a.getRawType()); + + List list1 = SubtypeModule.findSubtypes(a.getRawType(), a::getAnnotation); + List list2 = subtypes.getOrDefault(a.getRawType(), Collections.emptyList()); + + if (list1.isEmpty()) return list2; + if (list2.isEmpty()) return list1; + List list = new ArrayList<>(list1.size() + list2.size()); + list.addAll(list1); + list.addAll(list2); + return list; + } + }); + } + + /** + * load parent's subclass by SPI. + * + * @param parent parent class. + * @param parent class type. + */ + @SuppressWarnings("unchecked") + public void registerTypes(Class parent) { + if (subtypes.containsKey(parent)) { + return; + } + List> subclasses = new ArrayList<>(); + for (S instance : ServiceLoader.load(parent)) { + subclasses.add((Class) instance.getClass()); + } + this.registerTypes(parent, subclasses); + } + + /** + * register subtypes without SPI. + * Of course, you need to provide them :) + * + * @param parent: parent class. + * @param subclasses: children class. + * @param : parent class type. + */ + public void registerTypes(Class parent, Iterable> subclasses) { + List result = new ArrayList<>(); + for (Class subclass : subclasses) { + result.addAll(findSubtypes(subclass, subclass::getAnnotation)); + } + subtypes.put(parent, result); + } + + public void unregisterType(Class parent) { + subtypes.remove(parent); + } + + private static List findSubtypes(Class clazz, Function, JsonSubType> getter) { + if (clazz == null) { + return Collections.emptyList(); + } + JsonSubType subtype = getter.apply(JsonSubType.class); + if (subtype == null) { + return Collections.emptyList(); + } + List result = new ArrayList<>(); + result.add(new NamedType(clazz, subtype.value())); + // [databind#2761]: alternative set of names to use + for (String name : subtype.names()) { + result.add(new NamedType(clazz, name)); + } + return result; + } +} diff --git a/subtype/src/main/resources/META-INF/LICENSE b/subtype/src/main/resources/META-INF/LICENSE new file mode 100644 index 000000000..a9e546210 --- /dev/null +++ b/subtype/src/main/resources/META-INF/LICENSE @@ -0,0 +1,8 @@ +This copy of Jackson JSON processor `jackson-module-guice` module is licensed under the +Apache (Software) License, version 2.0 ("the License"). +See the License for details about distribution rights, and the +specific rights regarding derivative works. + +You may obtain a copy of the License at: + +http://www.apache.org/licenses/LICENSE-2.0 diff --git a/subtype/src/main/resources/META-INF/NOTICE b/subtype/src/main/resources/META-INF/NOTICE new file mode 100644 index 000000000..4c976b7b4 --- /dev/null +++ b/subtype/src/main/resources/META-INF/NOTICE @@ -0,0 +1,20 @@ +# Jackson JSON processor + +Jackson is a high-performance, Free/Open Source JSON processing library. +It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has +been in development since 2007. +It is currently developed by a community of developers, as well as supported +commercially by FasterXML.com. + +## Licensing + +Jackson core and extension components may licensed under different licenses. +To find the details that apply to this artifact see the accompanying LICENSE file. +For more information, including possible other licensing options, contact +FasterXML.com (http://fasterxml.com). + +## Credits + +A list of contributors may be found from CREDITS file, which is included +in some artifacts (usually source distributions); but is always available +from the source code management (SCM) system project uses. diff --git a/subtype/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module b/subtype/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module new file mode 100644 index 000000000..f0d3925eb --- /dev/null +++ b/subtype/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module @@ -0,0 +1 @@ +com.fasterxml.jackson.module.subtype.SubtypeModule \ No newline at end of file diff --git a/subtype/src/moditect/module-info.java b/subtype/src/moditect/module-info.java new file mode 100644 index 000000000..4988481f0 --- /dev/null +++ b/subtype/src/moditect/module-info.java @@ -0,0 +1,8 @@ +module com.fasterxml.jackson.module.subtype { + + requires com.fasterxml.jackson.core; + requires com.fasterxml.jackson.annotation; + requires com.fasterxml.jackson.databind; + + exports com.fasterxml.jackson.module.subtype; +} diff --git a/subtype/src/test/java/com/fasterxml/jackson/module/subtype/WithJsonSubTypesTest.java b/subtype/src/test/java/com/fasterxml/jackson/module/subtype/WithJsonSubTypesTest.java new file mode 100644 index 000000000..a7bf70a7a --- /dev/null +++ b/subtype/src/test/java/com/fasterxml/jackson/module/subtype/WithJsonSubTypesTest.java @@ -0,0 +1,273 @@ +package com.fasterxml.jackson.module.subtype; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.auto.service.AutoService; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +import static org.junit.Assert.*; + +/** + * test work with {@link JsonSubTypes} + */ +@RunWith(value = Parameterized.class) +public class WithJsonSubTypesTest { + + private final ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + + public static class Argument { + private final Class clazz; + private final T expected; + + public Argument(Class clazz, T expected) { + this.clazz = clazz; + this.expected = expected; + } + } + + @Parameter + public Argument argument; + + @Parameters + public static Collection> data() { + return Arrays.asList( + new Argument<>(FirstChild.class, new FirstChild("hello")), + new Argument<>(SecondChild.class, new SecondChild("world")), + new Argument<>(FirstAppendChild.class, new FirstAppendChild(42)), + new Argument<>(SecondAppendChild.class, new SecondAppendChild("42", Arrays.asList("hello", "foo", "bar"))), + new Argument<>(ThirdAppendChild.class, new ThirdAppendChild("42", Arrays.asList("hello", "foo", "bar"), 3.1415926)) + ); + } + + @Test + public void test() throws Exception { + final Parent parent = argument.expected; + String json = mapper.writeValueAsString(parent); + Parent unmarshal = mapper.readValue(json, Parent.class); + T actual = assertInstanceOf(argument.clazz, unmarshal); + assertEquals(argument.expected, actual); + } + + public static T assertInstanceOf(Class expectedType, Object actualValue) { + assertTrue(expectedType.isInstance(actualValue)); + return expectedType.cast(actualValue); + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") + @JsonSubTypes(value = { + @JsonSubTypes.Type(value = FirstChild.class, name = "first-child"), + @JsonSubTypes.Type(value = SecondChild.class, name = "second-child"), + }) + public interface Parent { + } + + public static class FirstChild implements Parent { + private String foo; + + @SuppressWarnings("unused") // SPI require it + public FirstChild() { + } + + public FirstChild(String foo) { + this.foo = foo; + } + + @SuppressWarnings("unused") // jackson require it + public String getFoo() { + return foo; + } + + @SuppressWarnings("unused") // jackson require it + public void setFoo(String foo) { + this.foo = foo; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + FirstChild that = (FirstChild) o; + + return Objects.equals(foo, that.foo); + } + + @Override + public int hashCode() { + return foo != null ? foo.hashCode() : 0; + } + } + + public static class SecondChild implements Parent { + private String bar; + + public SecondChild() { + } + + public SecondChild(String bar) { + this.bar = bar; + } + + @SuppressWarnings("unused") // jackson require it + public String getBar() { + return bar; + } + + @SuppressWarnings("unused") // jackson require it + public void setBar(String bar) { + this.bar = bar; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + SecondChild that = (SecondChild) o; + + return Objects.equals(bar, that.bar); + } + + @Override + public int hashCode() { + return bar != null ? bar.hashCode() : 0; + } + } + + @JsonSubType("first-append-child") + @AutoService(Parent.class) + public static class FirstAppendChild implements Parent { + private Integer integer; + + @SuppressWarnings("unused") // SPI require it + public FirstAppendChild() { + } + + public FirstAppendChild(Integer integer) { + this.integer = integer; + } + + @SuppressWarnings("unused") // jackson require it + public Integer getInteger() { + return integer; + } + + @SuppressWarnings("unused") // jackson require it + public void setInteger(Integer integer) { + this.integer = integer; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + FirstAppendChild that = (FirstAppendChild) o; + + return Objects.equals(integer, that.integer); + } + + @Override + public int hashCode() { + return integer != null ? integer.hashCode() : 0; + } + } + + + @JsonSubType("second-append-child") + @AutoService(Parent.class) + public static class SecondAppendChild extends SecondChild { + private List list; + + public SecondAppendChild() { + } + + public SecondAppendChild(String bar, List list) { + super(bar); + this.list = list; + } + + @SuppressWarnings("unused") // jackson require it + public List getList() { + return list; + } + + @SuppressWarnings("unused") // jackson require it + public void setList(List list) { + this.list = list; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + + SecondAppendChild that = (SecondAppendChild) o; + + return Objects.equals(list, that.list); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + (list != null ? list.hashCode() : 0); + return result; + } + } + + @JsonSubType("third-append-child") + @AutoService(Parent.class) + public static class ThirdAppendChild extends SecondAppendChild { + private double value; + + @SuppressWarnings("unused") // SPI require it + public ThirdAppendChild() { + } + + public ThirdAppendChild(String bar, List list, double value) { + super(bar, list); + this.value = value; + } + + @SuppressWarnings("unused") // jackson require it + public double getValue() { + return value; + } + + @SuppressWarnings("unused") // jackson require it + public void setValue(double value) { + this.value = value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + + ThirdAppendChild that = (ThirdAppendChild) o; + + return Double.compare(value, that.value) == 0; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + long temp; + temp = Double.doubleToLongBits(value); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + return result; + } + } +} \ No newline at end of file diff --git a/subtype/src/test/java/com/fasterxml/jackson/module/subtype/WithoutJsonSubTypesTest.java b/subtype/src/test/java/com/fasterxml/jackson/module/subtype/WithoutJsonSubTypesTest.java new file mode 100644 index 000000000..909754f6d --- /dev/null +++ b/subtype/src/test/java/com/fasterxml/jackson/module/subtype/WithoutJsonSubTypesTest.java @@ -0,0 +1,121 @@ +package com.fasterxml.jackson.module.subtype; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.auto.service.AutoService; +import org.junit.Test; + +import java.util.Objects; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * test work without {@link JsonSubTypes} + */ +public class WithoutJsonSubTypesTest { + private final ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + + @Test + public void testFirstChild() throws Exception { + FirstChild child = new FirstChild(); + child.setFoo("hello"); + String json = mapper.writeValueAsString(child); + + // {"type":"first-child","foo":"hello"} + + Parent unmarshal = mapper.readValue(json, Parent.class); + FirstChild actual = assertInstanceOf(FirstChild.class, unmarshal); + assertEquals("hello", actual.getFoo()); + } + + @Test + public void testSecondChild() throws Exception { + SecondChild child = new SecondChild(); + child.setBar("world"); + String json = mapper.writeValueAsString(child); + + // {"type":"second-child","bar":"world"} + + Parent unmarshal = mapper.readValue(json, Parent.class); + SecondChild actual = assertInstanceOf(SecondChild.class, unmarshal); + assertEquals("world", actual.getBar()); + } + + public static T assertInstanceOf(Class expectedType, Object actualValue) { + assertTrue(expectedType.isInstance(actualValue)); + return expectedType.cast(actualValue); + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") + public interface Parent { + } + + @JsonSubType("first-child") + @AutoService(Parent.class) // module requires spi + public static class FirstChild implements Parent { + private String foo; + + public FirstChild() { + } + + public String getFoo() { + return foo; + } + + public void setFoo(String foo) { + this.foo = foo; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + FirstChild that = (FirstChild) o; + + return Objects.equals(foo, that.foo); + } + + @Override + public int hashCode() { + return foo != null ? foo.hashCode() : 0; + } + } + + + @JsonSubType("second-child") + @AutoService(Parent.class) // module requires spi + public static class SecondChild implements Parent { + private String bar; + + public SecondChild() { + } + + public String getBar() { + return bar; + } + + public void setBar(String bar) { + this.bar = bar; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + SecondChild that = (SecondChild) o; + + return Objects.equals(bar, that.bar); + } + + @Override + public int hashCode() { + return bar != null ? bar.hashCode() : 0; + } + } + + +} \ No newline at end of file From e9ee00536aa0526d50abff1f1b1d0d6a2d67b844 Mon Sep 17 00:00:00 2001 From: black Date: Mon, 13 Nov 2023 09:53:24 +0800 Subject: [PATCH 4/8] fix --- subtype/README.md | 4 +- .../jackson/module/subtype/JsonSubType.java | 1 - .../jackson/module/subtype/SubtypeModule.java | 8 ++- .../module/subtype/WithJsonSubTypesTest.java | 64 ++----------------- .../subtype/WithoutJsonSubTypesTest.java | 32 +++------- 5 files changed, 22 insertions(+), 87 deletions(-) diff --git a/subtype/README.md b/subtype/README.md index 10ee00a1a..b23475c64 100644 --- a/subtype/README.md +++ b/subtype/README.md @@ -10,7 +10,7 @@ Implementation on SPI. Registering modules. ``` -ObjectMapper mapper = new ObjectMapper().registerModule(new DynamicSubtypeModule()); +ObjectMapper mapper = new ObjectMapper().registerModule(new SubtypeModule()); ``` Ensure that the parent class has at least the `JsonTypeInfo` annotation. @@ -25,7 +25,7 @@ public interface Parent { 2. provide a non-argument constructor (SPI require it). ```java -import io.github.black.jackson.JsonSubType; +import com.fasterxml.jackson.module.subtype.JsonSubType; @JsonSubType("first-child") public class FirstChild { diff --git a/subtype/src/main/java/com/fasterxml/jackson/module/subtype/JsonSubType.java b/subtype/src/main/java/com/fasterxml/jackson/module/subtype/JsonSubType.java index 4d499fada..842d17bb0 100644 --- a/subtype/src/main/java/com/fasterxml/jackson/module/subtype/JsonSubType.java +++ b/subtype/src/main/java/com/fasterxml/jackson/module/subtype/JsonSubType.java @@ -35,7 +35,6 @@ * more than one type name should be associated with the same type. * * @return subtype name array - * @since 2.12 */ String[] names() default {}; } diff --git a/subtype/src/main/java/com/fasterxml/jackson/module/subtype/SubtypeModule.java b/subtype/src/main/java/com/fasterxml/jackson/module/subtype/SubtypeModule.java index e1f7ab4a4..9b18605c2 100644 --- a/subtype/src/main/java/com/fasterxml/jackson/module/subtype/SubtypeModule.java +++ b/subtype/src/main/java/com/fasterxml/jackson/module/subtype/SubtypeModule.java @@ -23,6 +23,8 @@ *

* When not found in the cache, it loads and caches subclasses using SPI. * Therefore, we can {@link #unregisterType} a class and then module will reload this class's subclasses. + * + * @since 2.16 */ public class SubtypeModule extends Module { @@ -50,7 +52,7 @@ public Version version() { public List findSubtypes(Annotated a) { registerTypes(a.getRawType()); - List list1 = SubtypeModule.findSubtypes(a.getRawType(), a::getAnnotation); + List list1 = _findSubtypes(a.getRawType(), a::getAnnotation); List list2 = subtypes.getOrDefault(a.getRawType(), Collections.emptyList()); if (list1.isEmpty()) return list2; @@ -92,7 +94,7 @@ public void registerTypes(Class parent) { public void registerTypes(Class parent, Iterable> subclasses) { List result = new ArrayList<>(); for (Class subclass : subclasses) { - result.addAll(findSubtypes(subclass, subclass::getAnnotation)); + result.addAll(_findSubtypes(subclass, subclass::getAnnotation)); } subtypes.put(parent, result); } @@ -101,7 +103,7 @@ public void unregisterType(Class parent) { subtypes.remove(parent); } - private static List findSubtypes(Class clazz, Function, JsonSubType> getter) { + private List _findSubtypes(Class clazz, Function, JsonSubType> getter) { if (clazz == null) { return Collections.emptyList(); } diff --git a/subtype/src/test/java/com/fasterxml/jackson/module/subtype/WithJsonSubTypesTest.java b/subtype/src/test/java/com/fasterxml/jackson/module/subtype/WithJsonSubTypesTest.java index a7bf70a7a..eb3e665de 100644 --- a/subtype/src/test/java/com/fasterxml/jackson/module/subtype/WithJsonSubTypesTest.java +++ b/subtype/src/test/java/com/fasterxml/jackson/module/subtype/WithJsonSubTypesTest.java @@ -18,12 +18,12 @@ import static org.junit.Assert.*; /** - * test work with {@link JsonSubTypes} + * test {@link JsonSubType} work with {@link JsonSubTypes} */ @RunWith(value = Parameterized.class) public class WithJsonSubTypesTest { - private final ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + private final ObjectMapper mapper = new ObjectMapper().registerModule(new SubtypeModule()); public static class Argument { private final Class clazz; @@ -72,7 +72,7 @@ public interface Parent { } public static class FirstChild implements Parent { - private String foo; + public String foo; @SuppressWarnings("unused") // SPI require it public FirstChild() { @@ -82,16 +82,6 @@ public FirstChild(String foo) { this.foo = foo; } - @SuppressWarnings("unused") // jackson require it - public String getFoo() { - return foo; - } - - @SuppressWarnings("unused") // jackson require it - public void setFoo(String foo) { - this.foo = foo; - } - @Override public boolean equals(Object o) { if (this == o) return true; @@ -109,7 +99,7 @@ public int hashCode() { } public static class SecondChild implements Parent { - private String bar; + public String bar; public SecondChild() { } @@ -118,16 +108,6 @@ public SecondChild(String bar) { this.bar = bar; } - @SuppressWarnings("unused") // jackson require it - public String getBar() { - return bar; - } - - @SuppressWarnings("unused") // jackson require it - public void setBar(String bar) { - this.bar = bar; - } - @Override public boolean equals(Object o) { if (this == o) return true; @@ -147,7 +127,7 @@ public int hashCode() { @JsonSubType("first-append-child") @AutoService(Parent.class) public static class FirstAppendChild implements Parent { - private Integer integer; + public Integer integer; @SuppressWarnings("unused") // SPI require it public FirstAppendChild() { @@ -157,16 +137,6 @@ public FirstAppendChild(Integer integer) { this.integer = integer; } - @SuppressWarnings("unused") // jackson require it - public Integer getInteger() { - return integer; - } - - @SuppressWarnings("unused") // jackson require it - public void setInteger(Integer integer) { - this.integer = integer; - } - @Override public boolean equals(Object o) { if (this == o) return true; @@ -187,7 +157,7 @@ public int hashCode() { @JsonSubType("second-append-child") @AutoService(Parent.class) public static class SecondAppendChild extends SecondChild { - private List list; + public List list; public SecondAppendChild() { } @@ -197,16 +167,6 @@ public SecondAppendChild(String bar, List list) { this.list = list; } - @SuppressWarnings("unused") // jackson require it - public List getList() { - return list; - } - - @SuppressWarnings("unused") // jackson require it - public void setList(List list) { - this.list = list; - } - @Override public boolean equals(Object o) { if (this == o) return true; @@ -229,7 +189,7 @@ public int hashCode() { @JsonSubType("third-append-child") @AutoService(Parent.class) public static class ThirdAppendChild extends SecondAppendChild { - private double value; + public double value; @SuppressWarnings("unused") // SPI require it public ThirdAppendChild() { @@ -240,16 +200,6 @@ public ThirdAppendChild(String bar, List list, double value) { this.value = value; } - @SuppressWarnings("unused") // jackson require it - public double getValue() { - return value; - } - - @SuppressWarnings("unused") // jackson require it - public void setValue(double value) { - this.value = value; - } - @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/subtype/src/test/java/com/fasterxml/jackson/module/subtype/WithoutJsonSubTypesTest.java b/subtype/src/test/java/com/fasterxml/jackson/module/subtype/WithoutJsonSubTypesTest.java index 909754f6d..77056f9aa 100644 --- a/subtype/src/test/java/com/fasterxml/jackson/module/subtype/WithoutJsonSubTypesTest.java +++ b/subtype/src/test/java/com/fasterxml/jackson/module/subtype/WithoutJsonSubTypesTest.java @@ -12,35 +12,35 @@ import static org.junit.Assert.assertTrue; /** - * test work without {@link JsonSubTypes} + * test {@link JsonSubType} works alone, without {@link JsonSubTypes} */ public class WithoutJsonSubTypesTest { - private final ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + private final ObjectMapper mapper = new ObjectMapper().registerModule(new SubtypeModule()); @Test public void testFirstChild() throws Exception { FirstChild child = new FirstChild(); - child.setFoo("hello"); + child.foo = "hello"; String json = mapper.writeValueAsString(child); // {"type":"first-child","foo":"hello"} Parent unmarshal = mapper.readValue(json, Parent.class); FirstChild actual = assertInstanceOf(FirstChild.class, unmarshal); - assertEquals("hello", actual.getFoo()); + assertEquals("hello", actual.foo); } @Test public void testSecondChild() throws Exception { SecondChild child = new SecondChild(); - child.setBar("world"); + child.bar = "world"; String json = mapper.writeValueAsString(child); // {"type":"second-child","bar":"world"} Parent unmarshal = mapper.readValue(json, Parent.class); SecondChild actual = assertInstanceOf(SecondChild.class, unmarshal); - assertEquals("world", actual.getBar()); + assertEquals("world", actual.bar); } public static T assertInstanceOf(Class expectedType, Object actualValue) { @@ -55,19 +55,11 @@ public interface Parent { @JsonSubType("first-child") @AutoService(Parent.class) // module requires spi public static class FirstChild implements Parent { - private String foo; + public String foo; public FirstChild() { } - public String getFoo() { - return foo; - } - - public void setFoo(String foo) { - this.foo = foo; - } - @Override public boolean equals(Object o) { if (this == o) return true; @@ -88,19 +80,11 @@ public int hashCode() { @JsonSubType("second-child") @AutoService(Parent.class) // module requires spi public static class SecondChild implements Parent { - private String bar; + public String bar; public SecondChild() { } - public String getBar() { - return bar; - } - - public void setBar(String bar) { - this.bar = bar; - } - @Override public boolean equals(Object o) { if (this == o) return true; From 0328fc3a0d3b8bfca8b38b5fd253e5b17ffdf74c Mon Sep 17 00:00:00 2001 From: black-06 Date: Fri, 5 Sep 2025 08:53:22 +0800 Subject: [PATCH 5/8] update version --- subtype/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/subtype/pom.xml b/subtype/pom.xml index 99c921899..7152d6ad9 100644 --- a/subtype/pom.xml +++ b/subtype/pom.xml @@ -10,7 +10,7 @@ com.fasterxml.jackson.module jackson-modules-base - 2.16.0-SNAPSHOT + 2.21.0-SNAPSHOT jackson-module-subtype Jackson module: Subtype Annotation Support From 33e061440b97d4fbe3cc1a36b9a5bf9fd8470cb5 Mon Sep 17 00:00:00 2001 From: black-06 Date: Fri, 5 Sep 2025 09:23:31 +0800 Subject: [PATCH 6/8] migrate tests to JUnit 5 --- .../module/subtype/WithJsonSubTypesTest.java | 31 ++++++------------- .../subtype/WithoutJsonSubTypesTest.java | 7 +++-- 2 files changed, 14 insertions(+), 24 deletions(-) diff --git a/subtype/src/test/java/com/fasterxml/jackson/module/subtype/WithJsonSubTypesTest.java b/subtype/src/test/java/com/fasterxml/jackson/module/subtype/WithJsonSubTypesTest.java index eb3e665de..55732b096 100644 --- a/subtype/src/test/java/com/fasterxml/jackson/module/subtype/WithJsonSubTypesTest.java +++ b/subtype/src/test/java/com/fasterxml/jackson/module/subtype/WithJsonSubTypesTest.java @@ -4,23 +4,19 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.auto.service.AutoService; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameter; -import org.junit.runners.Parameterized.Parameters; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import java.util.Arrays; -import java.util.Collection; import java.util.List; import java.util.Objects; +import java.util.stream.Stream; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; /** * test {@link JsonSubType} work with {@link JsonSubTypes} */ -@RunWith(value = Parameterized.class) public class WithJsonSubTypesTest { private final ObjectMapper mapper = new ObjectMapper().registerModule(new SubtypeModule()); @@ -35,12 +31,8 @@ public Argument(Class clazz, T expected) { } } - @Parameter - public Argument argument; - - @Parameters - public static Collection> data() { - return Arrays.asList( + public static Stream> data() { + return Stream.of( new Argument<>(FirstChild.class, new FirstChild("hello")), new Argument<>(SecondChild.class, new SecondChild("world")), new Argument<>(FirstAppendChild.class, new FirstAppendChild(42)), @@ -49,8 +41,9 @@ public static Collection> data() { ); } - @Test - public void test() throws Exception { + @ParameterizedTest + @MethodSource("data") + void test(Argument argument) throws Exception { final Parent parent = argument.expected; String json = mapper.writeValueAsString(parent); Parent unmarshal = mapper.readValue(json, Parent.class); @@ -202,21 +195,17 @@ public ThirdAppendChild(String bar, List list, double value) { @Override public boolean equals(Object o) { - if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; if (!super.equals(o)) return false; ThirdAppendChild that = (ThirdAppendChild) o; - return Double.compare(value, that.value) == 0; } @Override public int hashCode() { int result = super.hashCode(); - long temp; - temp = Double.doubleToLongBits(value); - result = 31 * result + (int) (temp ^ (temp >>> 32)); + result = 31 * result + Double.hashCode(value); return result; } } diff --git a/subtype/src/test/java/com/fasterxml/jackson/module/subtype/WithoutJsonSubTypesTest.java b/subtype/src/test/java/com/fasterxml/jackson/module/subtype/WithoutJsonSubTypesTest.java index 77056f9aa..f023b8e1e 100644 --- a/subtype/src/test/java/com/fasterxml/jackson/module/subtype/WithoutJsonSubTypesTest.java +++ b/subtype/src/test/java/com/fasterxml/jackson/module/subtype/WithoutJsonSubTypesTest.java @@ -4,12 +4,13 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.auto.service.AutoService; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.Objects; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + /** * test {@link JsonSubType} works alone, without {@link JsonSubTypes} From d686a4f1cfe4ef9624bfcfcaeae3f33b844f6077 Mon Sep 17 00:00:00 2001 From: black-06 Date: Fri, 12 Sep 2025 09:24:45 +0800 Subject: [PATCH 7/8] update license --- subtype/src/main/resources/META-INF/LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/subtype/src/main/resources/META-INF/LICENSE b/subtype/src/main/resources/META-INF/LICENSE index a9e546210..c68da15d8 100644 --- a/subtype/src/main/resources/META-INF/LICENSE +++ b/subtype/src/main/resources/META-INF/LICENSE @@ -1,4 +1,4 @@ -This copy of Jackson JSON processor `jackson-module-guice` module is licensed under the +This copy of Jackson JSON processor `jackson-module-subtype` module is licensed under the Apache (Software) License, version 2.0 ("the License"). See the License for details about distribution rights, and the specific rights regarding derivative works. From 153e8ac580d29984782429762b6e21bf308d9411 Mon Sep 17 00:00:00 2001 From: black-06 Date: Fri, 12 Sep 2025 11:36:27 +0800 Subject: [PATCH 8/8] extract SubtypeAnnotationIntrospector --- .../SubtypeAnnotationIntrospector.java | 116 ++++++++++++++++++ .../jackson/module/subtype/SubtypeModule.java | 108 ++-------------- 2 files changed, 127 insertions(+), 97 deletions(-) create mode 100644 subtype/src/main/java/com/fasterxml/jackson/module/subtype/SubtypeAnnotationIntrospector.java diff --git a/subtype/src/main/java/com/fasterxml/jackson/module/subtype/SubtypeAnnotationIntrospector.java b/subtype/src/main/java/com/fasterxml/jackson/module/subtype/SubtypeAnnotationIntrospector.java new file mode 100644 index 000000000..1f77b4032 --- /dev/null +++ b/subtype/src/main/java/com/fasterxml/jackson/module/subtype/SubtypeAnnotationIntrospector.java @@ -0,0 +1,116 @@ +package com.fasterxml.jackson.module.subtype; + +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.databind.AnnotationIntrospector; +import com.fasterxml.jackson.databind.introspect.Annotated; +import com.fasterxml.jackson.databind.jsontype.NamedType; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.ServiceLoader; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +/** + * Annotation introspector that handles {@link JsonSubType} annotation. + *

+ * It caches the subclasses of a parent class, so it's not-real-time. + * When the parent class not found in cache, + * it will try to load all found child classes via SPI then cache it. + * We can remove the parent class in the cache by {@link #unregisterType}. + *

+ */ +public class SubtypeAnnotationIntrospector extends AnnotationIntrospector { + private final ConcurrentHashMap, List> subtypes = new ConcurrentHashMap<>(); + + @Override + public Version version() { + return PackageVersion.VERSION; + } + + @Override + public List findSubtypes(Annotated a) { + registerTypes(a.getRawType()); + + List list1 = _findSubtypes(a.getRawType(), a::getAnnotation); + List list2 = subtypes.getOrDefault(a.getRawType(), Collections.emptyList()); + + if (list1.isEmpty()) return list2; + if (list2.isEmpty()) return list1; + List list = new ArrayList<>(list1.size() + list2.size()); + list.addAll(list1); + list.addAll(list2); + return list; + } + + /** + * load parent's subclass by SPI. + * + * @param parent parent class. + * @param parent class type. + */ + @SuppressWarnings("unchecked") + public void registerTypes(Class parent) { + // If parent is already registered (either by spi or manually by the user), then skip it + if (subtypes.containsKey(parent)) { + return; + } + List> subclasses = new ArrayList<>(); + for (S instance : ServiceLoader.load(parent)) { + subclasses.add((Class) instance.getClass()); + } + this.registerTypes(parent, subclasses); + } + + /** + * register subtypes without SPI. + * Of course, you need to provide them :) + * + * @param parent: parent class. + * @param subclasses: children class. + * @param : parent class type. + */ + public void registerTypes(Class parent, Iterable> subclasses) { + List result = new ArrayList<>(); + for (Class subclass : subclasses) { + result.addAll(_findSubtypes(subclass, subclass::getAnnotation)); + } + subtypes.put(parent, result); + } + + /** + * remove the parent class in the cache, + * so that {@link #registerTypes(Class)} can re-look by SPI. + * + * @param parent: parent class. + */ + public void unregisterType(Class parent) { + subtypes.remove(parent); + } + + /** + * find all {@link JsonSubType} names. + * + * @param clazz class which annotate with {@link JsonSubType}. + * @param getter getAnnotation. + * @param class type. + * @return all names. + */ + private List _findSubtypes(Class clazz, Function, JsonSubType> getter) { + if (clazz == null) { + return Collections.emptyList(); + } + JsonSubType subtype = getter.apply(JsonSubType.class); + if (subtype == null) { + return Collections.emptyList(); + } + List result = new ArrayList<>(); + result.add(new NamedType(clazz, subtype.value())); + // [databind#2761]: alternative set of names to use + for (String name : subtype.names()) { + result.add(new NamedType(clazz, name)); + } + return result; + } +} diff --git a/subtype/src/main/java/com/fasterxml/jackson/module/subtype/SubtypeModule.java b/subtype/src/main/java/com/fasterxml/jackson/module/subtype/SubtypeModule.java index 9b18605c2..055f5b328 100644 --- a/subtype/src/main/java/com/fasterxml/jackson/module/subtype/SubtypeModule.java +++ b/subtype/src/main/java/com/fasterxml/jackson/module/subtype/SubtypeModule.java @@ -1,34 +1,23 @@ package com.fasterxml.jackson.module.subtype; import com.fasterxml.jackson.core.Version; -import com.fasterxml.jackson.databind.AnnotationIntrospector; import com.fasterxml.jackson.databind.Module; -import com.fasterxml.jackson.databind.introspect.Annotated; -import com.fasterxml.jackson.databind.jsontype.NamedType; -import com.fasterxml.jackson.module.subtype.PackageVersion; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.ServiceLoader; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Function; /** - * Subtype module. - *

- * The module caches the subclass, so it's non-real-time. - * It's for registering subtypes without annotating the parent class. + * Subtype module for registering subtypes without annotating the parent class. * See this issues in jackson-databind. - *

- * When not found in the cache, it loads and caches subclasses using SPI. - * Therefore, we can {@link #unregisterType} a class and then module will reload this class's subclasses. - * - * @since 2.16 */ public class SubtypeModule extends Module { - private final ConcurrentHashMap, List> subtypes = new ConcurrentHashMap<>(); + protected SubtypeAnnotationIntrospector _introspector; + + public SubtypeModule() { + this(new SubtypeAnnotationIntrospector()); + } + + public SubtypeModule(SubtypeAnnotationIntrospector introspector) { + this._introspector = introspector; + } @Override public String getModuleName() { @@ -42,81 +31,6 @@ public Version version() { @Override public void setupModule(SetupContext context) { - context.insertAnnotationIntrospector(new AnnotationIntrospector() { - @Override - public Version version() { - return PackageVersion.VERSION; - } - - @Override - public List findSubtypes(Annotated a) { - registerTypes(a.getRawType()); - - List list1 = _findSubtypes(a.getRawType(), a::getAnnotation); - List list2 = subtypes.getOrDefault(a.getRawType(), Collections.emptyList()); - - if (list1.isEmpty()) return list2; - if (list2.isEmpty()) return list1; - List list = new ArrayList<>(list1.size() + list2.size()); - list.addAll(list1); - list.addAll(list2); - return list; - } - }); - } - - /** - * load parent's subclass by SPI. - * - * @param parent parent class. - * @param parent class type. - */ - @SuppressWarnings("unchecked") - public void registerTypes(Class parent) { - if (subtypes.containsKey(parent)) { - return; - } - List> subclasses = new ArrayList<>(); - for (S instance : ServiceLoader.load(parent)) { - subclasses.add((Class) instance.getClass()); - } - this.registerTypes(parent, subclasses); - } - - /** - * register subtypes without SPI. - * Of course, you need to provide them :) - * - * @param parent: parent class. - * @param subclasses: children class. - * @param : parent class type. - */ - public void registerTypes(Class parent, Iterable> subclasses) { - List result = new ArrayList<>(); - for (Class subclass : subclasses) { - result.addAll(_findSubtypes(subclass, subclass::getAnnotation)); - } - subtypes.put(parent, result); - } - - public void unregisterType(Class parent) { - subtypes.remove(parent); - } - - private List _findSubtypes(Class clazz, Function, JsonSubType> getter) { - if (clazz == null) { - return Collections.emptyList(); - } - JsonSubType subtype = getter.apply(JsonSubType.class); - if (subtype == null) { - return Collections.emptyList(); - } - List result = new ArrayList<>(); - result.add(new NamedType(clazz, subtype.value())); - // [databind#2761]: alternative set of names to use - for (String name : subtype.names()) { - result.add(new NamedType(clazz, name)); - } - return result; + context.insertAnnotationIntrospector(_introspector); } }