Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ not datatype, data format, or JAX-RS provider modules.
<module>mrbean</module>
<module>osgi</module>
<module>paranamer</module>
<module>subtype</module>
Copy link
Member

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...

Copy link
Author

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...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True🥲🥲

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... we still alternative name. :)

<!-- since 2.13: -->
<module>no-ctor-deser</module>
</modules>
Expand Down
66 changes: 66 additions & 0 deletions subtype/README.md
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.
76 changes: 76 additions & 0 deletions subtype/pom.xml
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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Name sounds like it's part of @JsonSubTypes of annotations module. This may confuse users quite. If module name changes as per https://github.com/FasterXML/jackson-modules-base/pull/229/files#r1390329649, can we change name here also?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be renamed, yes. Too confusing otherwise.

I think it should use @Jackson prefix, not @Json, for one. Maybe @JacksonSubTypeName

/**
* 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);
}
}
8 changes: 8 additions & 0 deletions subtype/src/main/resources/META-INF/LICENSE
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
20 changes: 20 additions & 0 deletions subtype/src/main/resources/META-INF/NOTICE
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.
Loading