diff --git a/pom.xml b/pom.xml
index ff0ef865..ad886a64 100644
--- a/pom.xml
+++ b/pom.xml
@@ -33,6 +33,7 @@ not datatype, data format, or JAX-RS provider modules.
mrbeanosgiparanamer
+ subtypeno-ctor-deser
diff --git a/subtype/README.md b/subtype/README.md
new file mode 100644
index 00000000..b23475c6
--- /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 SubtypeModule());
+```
+
+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 com.fasterxml.jackson.module.subtype.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 00000000..99c92189
--- /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 00000000..842d17bb
--- /dev/null
+++ b/subtype/src/main/java/com/fasterxml/jackson/module/subtype/JsonSubType.java
@@ -0,0 +1,40 @@
+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
+ */
+ 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 00000000..7860aa14
--- /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 00000000..9b18605c
--- /dev/null
+++ b/subtype/src/main/java/com/fasterxml/jackson/module/subtype/SubtypeModule.java
@@ -0,0 +1,122 @@
+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.
+ *
+ * @since 2.16
+ */
+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 = _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;
+ }
+}
diff --git a/subtype/src/main/resources/META-INF/LICENSE b/subtype/src/main/resources/META-INF/LICENSE
new file mode 100644
index 00000000..a9e54621
--- /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 00000000..4c976b7b
--- /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 00000000..f0d3925e
--- /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 00000000..4988481f
--- /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 00000000..eb3e665d
--- /dev/null
+++ b/subtype/src/test/java/com/fasterxml/jackson/module/subtype/WithJsonSubTypesTest.java
@@ -0,0 +1,223 @@
+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 {@link JsonSubType} work with {@link JsonSubTypes}
+ */
+@RunWith(value = Parameterized.class)
+public class WithJsonSubTypesTest {
+
+ private final ObjectMapper mapper = new ObjectMapper().registerModule(new SubtypeModule());
+
+ 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 {
+ public String foo;
+
+ @SuppressWarnings("unused") // SPI require it
+ public FirstChild() {
+ }
+
+ public FirstChild(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 {
+ public String bar;
+
+ public SecondChild() {
+ }
+
+ public SecondChild(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 {
+ public Integer integer;
+
+ @SuppressWarnings("unused") // SPI require it
+ public FirstAppendChild() {
+ }
+
+ public FirstAppendChild(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 {
+ public List list;
+
+ public SecondAppendChild() {
+ }
+
+ public SecondAppendChild(String bar, List list) {
+ super(bar);
+ 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 {
+ public double value;
+
+ @SuppressWarnings("unused") // SPI require it
+ public ThirdAppendChild() {
+ }
+
+ public ThirdAppendChild(String bar, List list, double value) {
+ super(bar, list);
+ 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 00000000..77056f9a
--- /dev/null
+++ b/subtype/src/test/java/com/fasterxml/jackson/module/subtype/WithoutJsonSubTypesTest.java
@@ -0,0 +1,105 @@
+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 {@link JsonSubType} works alone, without {@link JsonSubTypes}
+ */
+public class WithoutJsonSubTypesTest {
+ private final ObjectMapper mapper = new ObjectMapper().registerModule(new SubtypeModule());
+
+ @Test
+ public void testFirstChild() throws Exception {
+ FirstChild child = new FirstChild();
+ 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.foo);
+ }
+
+ @Test
+ public void testSecondChild() throws Exception {
+ SecondChild child = new SecondChild();
+ 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.bar);
+ }
+
+ 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 {
+ public String foo;
+
+ public FirstChild() {
+ }
+
+ @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 {
+ public String bar;
+
+ public SecondChild() {
+ }
+
+ @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