-
Notifications
You must be signed in to change notification settings - Fork 77
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add subtype module (2.17) #229
base: 2.16
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think current name is |
||
``` | ||
|
||
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Needs to change to new package. |
||
|
||
@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. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
<!-- This module was also published with a richer model, Gradle metadata, --> | ||
<!-- which should be used instead. Do not delete the following line which --> | ||
<!-- is to indicate to Gradle or any Gradle module metadata file consumer --> | ||
<!-- that they should prefer consuming it instead. --> | ||
<!-- do_not_remove: published-with-gradle-metadata --> | ||
<modelVersion>4.0.0</modelVersion> | ||
<parent> | ||
<groupId>com.fasterxml.jackson.module</groupId> | ||
<artifactId>jackson-modules-base</artifactId> | ||
<version>2.16.0-SNAPSHOT</version> | ||
</parent> | ||
<artifactId>jackson-module-subtype</artifactId> | ||
<name>Jackson module: Subtype Annotation Support</name> | ||
<packaging>bundle</packaging> | ||
|
||
<description>Registering subtypes without annotating the parent class</description> | ||
<url>https://github.com/FasterXML/jackson-modules-base</url> | ||
|
||
<licenses> | ||
<license> | ||
<name>The Apache Software License, Version 2.0</name> | ||
<url>https://www.apache.org/licenses/LICENSE-2.0.txt</url> | ||
<distribution>repo</distribution> | ||
</license> | ||
</licenses> | ||
|
||
<properties> | ||
<!-- Generate PackageVersion.java into this directory. --> | ||
<packageVersion.dir>com/fasterxml/jackson/module/subtype</packageVersion.dir> | ||
<packageVersion.package>com.fasterxml.jackson.module.subtype</packageVersion.package> | ||
</properties> | ||
|
||
<dependencies> | ||
<dependency> | ||
<groupId>com.fasterxml.jackson.core</groupId> | ||
<artifactId>jackson-core</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>com.fasterxml.jackson.core</groupId> | ||
<artifactId>jackson-databind</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>com.google.auto.service</groupId> | ||
<artifactId>auto-service</artifactId> | ||
<version>1.0.1</version> | ||
<scope>test</scope> | ||
</dependency> | ||
</dependencies> | ||
|
||
<build> | ||
<plugins> | ||
<plugin> | ||
<groupId>com.google.code.maven-replacer-plugin</groupId> | ||
<artifactId>replacer</artifactId> | ||
</plugin> | ||
<!-- 14-Mar-2019, tatu: Add rudimentary JDK9+ module info. To build with JDK 8 | ||
will have to use `moduleInfoFile` as anything else requires JDK 9+ | ||
--> | ||
<plugin> | ||
<groupId>org.moditect</groupId> | ||
<artifactId>moditect-maven-plugin</artifactId> | ||
</plugin> | ||
<plugin> | ||
<groupId>org.apache.maven.plugins</groupId> | ||
<artifactId>maven-compiler-plugin</artifactId> | ||
<configuration> | ||
<source>9</source> | ||
<target>9</target> | ||
</configuration> | ||
</plugin> | ||
</plugins> | ||
</build> | ||
</project> |
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
@@ -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. | ||||
* <p> | ||||
* It's the same as {@link JsonSubTypes.Type}. | ||||
*/ | ||||
@Target(ElementType.TYPE) | ||||
@Retention(RetentionPolicy.RUNTIME) | ||||
@JacksonAnnotation | ||||
public @interface JsonSubType { | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Name sounds like it's part of |
||||
/** | ||||
* 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 | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Version mismatch, can remove.
Suggested change
... then add |
||||
*/ | ||||
String[] names() default {}; | ||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -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. | ||||||||||||||||||
* <p> | ||||||||||||||||||
* The module caches the subclass, so it's non-real-time. | ||||||||||||||||||
* It's for registering subtypes without annotating the parent class. | ||||||||||||||||||
* See <a href="https://github.com/FasterXML/jackson-databind/issues/2104">this issues</a> in jackson-databind. | ||||||||||||||||||
* <p> | ||||||||||||||||||
* 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 { | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||
|
||||||||||||||||||
private final ConcurrentHashMap<Class<?>, List<NamedType>> 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<NamedType> findSubtypes(Annotated a) { | ||||||||||||||||||
registerTypes(a.getRawType()); | ||||||||||||||||||
|
||||||||||||||||||
List<NamedType> list1 = SubtypeModule.findSubtypes(a.getRawType(), a::getAnnotation); | ||||||||||||||||||
List<NamedType> list2 = subtypes.getOrDefault(a.getRawType(), Collections.emptyList()); | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we rename these There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||||||||||||||
|
||||||||||||||||||
if (list1.isEmpty()) return list2; | ||||||||||||||||||
if (list2.isEmpty()) return list1; | ||||||||||||||||||
Comment on lines
+58
to
+59
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the two lists are null-safe, no need for empty checking. |
||||||||||||||||||
List<NamedType> list = new ArrayList<>(list1.size() + list2.size()); | ||||||||||||||||||
list.addAll(list1); | ||||||||||||||||||
list.addAll(list2); | ||||||||||||||||||
return list; | ||||||||||||||||||
Comment on lines
+60
to
+63
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Comment on lines
+60
to
+63
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it possible to have duplication? If so, can we have these as set first, then return as list? |
||||||||||||||||||
} | ||||||||||||||||||
}); | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
/** | ||||||||||||||||||
* load parent's subclass by SPI. | ||||||||||||||||||
* | ||||||||||||||||||
* @param parent parent class. | ||||||||||||||||||
* @param <S> parent class type. | ||||||||||||||||||
*/ | ||||||||||||||||||
@SuppressWarnings("unchecked") | ||||||||||||||||||
public <S> void registerTypes(Class<S> parent) { | ||||||||||||||||||
if (subtypes.containsKey(parent)) { | ||||||||||||||||||
return; | ||||||||||||||||||
} | ||||||||||||||||||
List<Class<S>> subclasses = new ArrayList<>(); | ||||||||||||||||||
for (S instance : ServiceLoader.load(parent)) { | ||||||||||||||||||
subclasses.add((Class<S>) 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 <S>: parent class type. | ||||||||||||||||||
*/ | ||||||||||||||||||
public <S> void registerTypes(Class<S> parent, Iterable<Class<S>> subclasses) { | ||||||||||||||||||
List<NamedType> result = new ArrayList<>(); | ||||||||||||||||||
for (Class<S> subclass : subclasses) { | ||||||||||||||||||
result.addAll(findSubtypes(subclass, subclass::getAnnotation)); | ||||||||||||||||||
} | ||||||||||||||||||
subtypes.put(parent, result); | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
public void unregisterType(Class<?> parent) { | ||||||||||||||||||
subtypes.remove(parent); | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
private static <S> List<NamedType> findSubtypes(Class<S> clazz, Function<Class<JsonSubType>, JsonSubType> getter) { | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a reason for keeping it static? if not maybe like...
Suggested change
|
||||||||||||||||||
if (clazz == null) { | ||||||||||||||||||
return Collections.emptyList(); | ||||||||||||||||||
} | ||||||||||||||||||
JsonSubType subtype = getter.apply(JsonSubType.class); | ||||||||||||||||||
if (subtype == null) { | ||||||||||||||||||
return Collections.emptyList(); | ||||||||||||||||||
} | ||||||||||||||||||
List<NamedType> 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; | ||||||||||||||||||
} | ||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ([email protected]), 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. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
com.fasterxml.jackson.module.subtype.SubtypeModule |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll probably want to change the name, "subtype" is too generic. But let me think about better name for a while...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a difficult thing for me to give it a name...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True🥲🥲