-
Notifications
You must be signed in to change notification settings - Fork 83
feat: add subtype module (2.17) #229
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
base: 2.x
Are you sure you want to change the base?
Changes from all commits
30da2f3
5152c26
ec05795
57009d8
e9ee005
f7f0cb6
0328fc3
33e0614
d686a4f
153e8ac
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 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. |
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.21.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,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. | ||
* <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 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. Should be renamed, yes. Too confusing otherwise. I think it should use |
||
/** | ||
* 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 {}; | ||
} |
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,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. | ||
* <p> | ||
* 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}. | ||
* </p> | ||
*/ | ||
public class SubtypeAnnotationIntrospector extends AnnotationIntrospector { | ||
private final ConcurrentHashMap<Class<?>, List<NamedType>> subtypes = new ConcurrentHashMap<>(); | ||
|
||
@Override | ||
public Version version() { | ||
return PackageVersion.VERSION; | ||
} | ||
|
||
@Override | ||
public List<NamedType> findSubtypes(Annotated a) { | ||
registerTypes(a.getRawType()); | ||
|
||
List<NamedType> list1 = _findSubtypes(a.getRawType(), a::getAnnotation); | ||
List<NamedType> list2 = subtypes.getOrDefault(a.getRawType(), Collections.emptyList()); | ||
|
||
if (list1.isEmpty()) return list2; | ||
if (list2.isEmpty()) return list1; | ||
List<NamedType> 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 <S> parent class type. | ||
*/ | ||
@SuppressWarnings("unchecked") | ||
public <S> void registerTypes(Class<S> parent) { | ||
// If parent is already registered (either by spi or manually by the user), then skip it | ||
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); | ||
} | ||
|
||
/** | ||
* 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 <S> class type. | ||
* @return all names. | ||
*/ | ||
private <S> List<NamedType> _findSubtypes(Class<S> clazz, Function<Class<JsonSubType>, JsonSubType> getter) { | ||
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,36 @@ | ||
package com.fasterxml.jackson.module.subtype; | ||
|
||
import com.fasterxml.jackson.core.Version; | ||
import com.fasterxml.jackson.databind.Module; | ||
|
||
/** | ||
* Subtype module 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. | ||
*/ | ||
public class SubtypeModule extends Module { | ||
|
||
protected SubtypeAnnotationIntrospector _introspector; | ||
|
||
public SubtypeModule() { | ||
this(new SubtypeAnnotationIntrospector()); | ||
} | ||
|
||
public SubtypeModule(SubtypeAnnotationIntrospector introspector) { | ||
this._introspector = introspector; | ||
} | ||
|
||
@Override | ||
public String getModuleName() { | ||
return getClass().getSimpleName(); | ||
} | ||
|
||
@Override | ||
public Version version() { | ||
return PackageVersion.VERSION; | ||
} | ||
|
||
@Override | ||
public void setupModule(SetupContext context) { | ||
context.insertAnnotationIntrospector(_introspector); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
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. | ||
|
||
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. |
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🥲🥲
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.
... we still alternative name. :)