If an object hierarchy should be serialized and deserialized to/from Json, type information has to be provided:
Example data structure:
class Wrapper {
Vehicle[] vehicles;
}
class Vehicle {
String name;
}
Car extends Vehicle {
String horsePower;
}
class Bicycle extends Vehicle {
String color;
}
With the help of the JsonTypeInfo
and JsonSubTypes
the class hierarchy can be (de)serialized. The annotations have to be
applied to the super-class.
The JsonSubTypes
with Type
annotations result in a cyclic dependency between super-class and sub-class.
The type info is stored in a property due to JsonTypeInfo.As.PROPERTY
and property = "@type"
:
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "@type")
@JsonSubTypes({ @Type(value = Car.class, name = "car"), @Type(value = Bicycle.class, name = "bicycle"), })
class Vehicle {
...
}
Example output:
{
"vehicles" : [ {
"@type" : "vehicle",
"name" : "myVehicle"
}, {
"@type" : "car",
"name" : "myCar",
"horsePower" : "23"
} ]
}
The same with type info as WRAPPER_OBJECT
:
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT, property = "@type")
@JsonSubTypes({ @Type(value = Car.class, name = "car"), @Type(value = Bicycle.class, name = "bicycle"), })
class Vehicle {
...
}
Example output:
{
"vehicles" : [ {
"my-vehicle" : {
"name" : "myVehicle"
}
}, {
"my-car" : {
"name" : "myCar",
"horsePower" : "99"
}
} ]
}
With ObjectMapper.registerSubtypes(...)
jackson provides the possibility to explicitly register subtypes. This has to be done
in the configuration step of the ObjectMapper
.
In order to simplify and generalize this functionality, we can use classpath scanning.
We use the JsonTypeName
annotation as marker for classes which should be registered as subtypes.
With this approach we do not have cyclic dependencies anymore. Furthermore, it is possible to add additional sub-classes from other modules / jars without the need of changing the configuration.
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "@type")
@JsonTypeName("vehicle")
class Vehicle {
...
}
@JsonTypeName("car")
Car extends Vehicle {
...
}
@JsonTypeName("bicycle")
class Bicycle extends Vehicle {
...
}
With the help of ClassPathScanningCandidateComponentProvider
and AnnotationTypeFilter
we can scan for all classes in
the classpath which have a JsonTypeName
annotation. From the resulting BeanDefinition
s it is straight forward to construct
a NamedType
:
List<Class<?>> findClasses(String basePackage) {
ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false);
provider.addIncludeFilter(new AnnotationTypeFilter(JsonTypeName.class));
return provider.findCandidateComponents(basePackage).stream().map(beanDefinition -> {
try {
return Class.forName(beanDefinition.getBeanClassName());
} catch (ClassNotFoundException e) {
throw new IllegalStateException("Class not found " + beanDefinition.getBeanClassName(), e);
}
}).collect(Collectors.toList());
}
See https://github.com/classgraph/classgraph
List<Class<?>> findClasses(String basePackage) {
try (ScanResult scanResult = new ClassGraph().enableClassInfo().enableAnnotationInfo()
.ignoreClassVisibility().whitelistPackages(basePackage).scan()) {
ClassInfoList classInfoList = scanResult.getClassesWithAnnotation(JsonTypeName.class.getName());
return classInfoList.loadClasses();
}
}