Skip to content

Commit c716d4f

Browse files
Sealed classes - alternative to @JsonSubTypes (#937)
1 parent 0c7a44f commit c716d4f

File tree

3 files changed

+149
-44
lines changed

3 files changed

+149
-44
lines changed

sample-gradle/build.gradle

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
11

22
buildscript {
3+
// /*dev*/ repositories {
4+
// /*dev*/ mavenLocal()
5+
// /*dev*/ }
36
dependencies {
4-
classpath 'com.fasterxml.jackson.module:jackson-module-scala_2.13:2.13.4'
7+
classpath 'com.fasterxml.jackson.module:jackson-module-scala_2.13:2.14.2'
8+
// /*dev*/ classpath 'cz.habarta.typescript-generator:typescript-generator-gradle-plugin:FILL_VERSION-SNAPSHOT'
59
}
610
}
711

812
plugins {
913
id 'java'
1014
id 'groovy'
11-
id "org.jetbrains.kotlin.jvm" version "1.7.21"
15+
id "org.jetbrains.kotlin.jvm" version "1.8.10"
1216
id 'scala'
13-
id 'cz.habarta.typescript-generator' version 'FILL_VERSION'
17+
/*prod*/ id 'cz.habarta.typescript-generator' version 'FILL_VERSION'
1418
}
1519

20+
// /*dev*/ apply plugin: 'cz.habarta.typescript-generator'
21+
1622
version = '3.0'
1723
sourceCompatibility = 11
1824
targetCompatibility = 11
@@ -22,10 +28,11 @@ repositories {
2228
}
2329

2430
dependencies {
25-
implementation 'com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider:2.13.4'
26-
implementation 'org.codehaus.groovy:groovy-all:3.0.13'
27-
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.7.21'
28-
implementation 'org.scala-lang:scala-library:2.13.9'
31+
implementation 'com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider:2.14.2'
32+
implementation 'org.codehaus.groovy:groovy-all:3.0.16'
33+
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.8.10'
34+
implementation 'org.scala-lang:scala-library:2.13.10'
35+
implementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.14.2'
2936
}
3037

3138
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile) {
@@ -52,7 +59,8 @@ generateTypeScript {
5259
'scala.Serializable',
5360
]
5461
jackson2Modules = [
55-
'com.fasterxml.jackson.module.scala.DefaultScalaModule'
62+
'com.fasterxml.jackson.module.scala.DefaultScalaModule',
63+
'com.fasterxml.jackson.module.kotlin.KotlinModule',
5664
]
5765
}
5866

typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/Jackson2Parser.java

Lines changed: 49 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,8 @@ private BeanModel parseBean(SourceType<Class<?>> sourceClass, List<String> class
300300

301301
final Pair<Class<?>, JsonTypeInfo> classWithJsonTypeInfo = Pair.of(sourceClass.type, sourceClass.type.getAnnotation(JsonTypeInfo.class));
302302
final Pair<Class<?>, JsonTypeInfo> parentClassWithJsonTypeInfo;
303-
if (isTaggedUnion(classWithJsonTypeInfo)) {
303+
final boolean isTaggedUnionParent = isTaggedUnion(classWithJsonTypeInfo);
304+
if (isTaggedUnionParent) {
304305
// this is parent
305306
final JsonTypeInfo jsonTypeInfo = classWithJsonTypeInfo.getValue2();
306307
discriminantProperty = getDiscriminantPropertyName(jsonTypeInfo);
@@ -336,16 +337,11 @@ private BeanModel parseBean(SourceType<Class<?>> sourceClass, List<String> class
336337
}
337338
}
338339

339-
final List<Class<?>> taggedUnionClasses;
340-
final JsonSubTypes jsonSubTypes = sourceClass.type.getAnnotation(JsonSubTypes.class);
341-
if (jsonSubTypes != null) {
342-
taggedUnionClasses = new ArrayList<>();
343-
for (JsonSubTypes.Type type : jsonSubTypes.value()) {
344-
addBeanToQueue(new SourceType<>(type.value(), sourceClass.type, "<subClass>"));
345-
taggedUnionClasses.add(type.value());
346-
}
347-
} else {
348-
taggedUnionClasses = null;
340+
final List<Class<?>> taggedUnionClasses = getSubClassesFromAnnotation(sourceClass.type)
341+
.or(() -> isTaggedUnionParent ? getSubClassesFromResolver(sourceClass.type) : Optional.empty())
342+
.orElse(null);
343+
if (taggedUnionClasses != null) {
344+
taggedUnionClasses.forEach(subClass -> addBeanToQueue(new SourceType<>(subClass, sourceClass.type, "<subClass>")));
349345
}
350346
final Type superclass = sourceClass.type.getGenericSuperclass() == Object.class ? null : sourceClass.type.getGenericSuperclass();
351347
if (superclass != null) {
@@ -444,46 +440,63 @@ private String getDiscriminantPropertyName(JsonTypeInfo jsonTypeInfo) {
444440
}
445441

446442
private String getTypeName(Class<?> cls) {
447-
final List<String> typeNames = getTypeNamesOrEmptyOrNull(cls);
448-
return typeNames != null && !typeNames.isEmpty() ? typeNames.get(0) : null;
449-
}
450-
451-
private List<String> getTypeNamesOrEmptyOrNull(Class<?> cls) {
452443
try {
453444
final SerializationConfig config = objectMapper.getSerializationConfig();
454445
final JavaType javaType = config.constructType(cls);
455446
final TypeSerializer typeSerializer = objectMapper.getSerializerProviderInstance().findTypeSerializer(javaType);
456447
final TypeIdResolver typeIdResolver = typeSerializer.getTypeIdResolver();
457448
if (typeIdResolver.getMechanism() == JsonTypeInfo.Id.NAME) {
458-
final SubtypeResolver subtypeResolver = config.getSubtypeResolver();
459-
final BeanDescription beanDescription = config.introspectClassAnnotations(cls);
460-
final AnnotatedClass annotatedClass = beanDescription.getClassInfo();
461-
final Collection<NamedType> serializationSubtypes = subtypeResolver.collectAndResolveSubtypesByClass(config, annotatedClass);
462-
final Collection<NamedType> deserializationSubtypes = subtypeResolver.collectAndResolveSubtypesByTypeId(config, annotatedClass);
463-
final List<String> serializationTypeNames = getTypeNamesFromSubtypes(serializationSubtypes, cls); // 0 or 1
464-
final List<String> deserializationTypeNames = getTypeNamesFromSubtypes(deserializationSubtypes, cls); // 0 or n
465-
final LinkedHashSet<String> typeNames = Stream
466-
.concat(serializationTypeNames.stream(), deserializationTypeNames.stream())
467-
.collect(Collectors.toCollection(LinkedHashSet::new));
468-
if (typeNames.isEmpty()) {
469-
return isInterfaceOrAbstract(cls) ? null : Utils.listFromNullable(typeIdResolver.idFromBaseType());
449+
final List<NamedType> subtypes = getSubtypesFromResolver(cls);
450+
final String typeName = subtypes.stream()
451+
.filter(subtype -> Objects.equals(subtype.getType(), cls))
452+
.filter(NamedType::hasName)
453+
.map(NamedType::getName)
454+
.findFirst()
455+
.orElse(null);
456+
if (typeName == null) {
457+
return isInterfaceOrAbstract(cls) ? null : typeIdResolver.idFromBaseType();
470458
} else {
471-
return new ArrayList<>(typeNames);
459+
return typeName;
472460
}
473461
} else {
474-
return Utils.listFromNullable(typeIdResolver.idFromBaseType());
462+
return typeIdResolver.idFromBaseType();
475463
}
476464
} catch (Exception e) {
477465
return null;
478466
}
479467
}
480468

481-
private static List<String> getTypeNamesFromSubtypes(Collection<NamedType> subtypes, Class<?> cls) {
482-
return subtypes.stream()
483-
.filter(subtype -> Objects.equals(subtype.getType(), cls))
484-
.filter(NamedType::hasName)
485-
.map(NamedType::getName)
486-
.collect(Collectors.toList());
469+
private Optional<List<Class<?>>> getSubClassesFromAnnotation(Class<?> cls) {
470+
return Optional.ofNullable(cls.getAnnotation(JsonSubTypes.class))
471+
.map(jsonSubTypes -> Arrays.stream(jsonSubTypes.value())
472+
.map(jsonSubType -> jsonSubType.value())
473+
.collect(Collectors.toList()));
474+
}
475+
476+
private Optional<List<Class<?>>> getSubClassesFromResolver(Class<?> cls) {
477+
final List<NamedType> subtypes = getSubtypesFromResolver(cls);
478+
final List<Class<?>> subClasses = subtypes.stream()
479+
.map(subtype -> subtype.getType())
480+
.filter(subClass -> !Objects.equals(subClass, cls))
481+
.collect(Collectors.toList());
482+
return subClasses.isEmpty() ? Optional.empty() : Optional.of(subClasses);
483+
}
484+
485+
/**
486+
* @return subtypes of specified class including the class itself
487+
*/
488+
private List<NamedType> getSubtypesFromResolver(Class<?> cls) {
489+
final SerializationConfig config = objectMapper.getSerializationConfig();
490+
final SubtypeResolver subtypeResolver = config.getSubtypeResolver();
491+
final BeanDescription beanDescription = config.introspectClassAnnotations(cls);
492+
final AnnotatedClass annotatedClass = beanDescription.getClassInfo();
493+
final Collection<NamedType> deserializationSubtypes = subtypeResolver.collectAndResolveSubtypesByTypeId(config, annotatedClass);
494+
final Collection<NamedType> serializationSubtypes = subtypeResolver.collectAndResolveSubtypesByClass(config, annotatedClass);
495+
final LinkedHashSet<NamedType> subtypes = Stream
496+
.concat(deserializationSubtypes.stream(), serializationSubtypes.stream())
497+
.filter(namedType -> cls.isAssignableFrom(namedType.getType())) // `SubtypeResolver` returns all types from `JsonSubTypes` annotations, not only subtypes
498+
.collect(Collectors.toCollection(LinkedHashSet::new));
499+
return new ArrayList<>(subtypes);
487500
}
488501

489502
private boolean isInterfaceOrAbstract(Class<?> cls) {
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
2+
package cz.habarta.typescript.generator;
3+
4+
import com.fasterxml.jackson.annotation.JsonTypeInfo;
5+
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;
6+
import com.fasterxml.jackson.annotation.JsonTypeName;
7+
import com.fasterxml.jackson.core.JsonProcessingException;
8+
import com.fasterxml.jackson.databind.ObjectMapper;
9+
import com.fasterxml.jackson.databind.introspect.Annotated;
10+
import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;
11+
import com.fasterxml.jackson.databind.jsontype.NamedType;
12+
import com.fasterxml.jackson.databind.module.SimpleModule;
13+
import java.util.Arrays;
14+
import java.util.List;
15+
import org.junit.jupiter.api.Assertions;
16+
import org.junit.jupiter.api.Test;
17+
18+
19+
public class SealedClassTest {
20+
21+
// TODO uncomment on Java 17
22+
23+
// @Test
24+
// public void testObjectMapper() throws JsonProcessingException {
25+
// final ObjectMapper objectMapper = new ObjectMapper();
26+
// objectMapper.registerModule(new SealedClassesModule());
27+
// final Shape shape = objectMapper.readValue(
28+
// """
29+
// {
30+
// "type": "circle",
31+
// "radius": 42
32+
// }
33+
// """,
34+
// Shape.class
35+
// );
36+
// Assertions.assertTrue(shape instanceof Circle);
37+
// Assertions.assertEquals(42, ((Circle)shape).radius);
38+
// }
39+
40+
// @Test
41+
// public void testSealedClassWithModule() throws JsonProcessingException {
42+
// final Settings settings = TestUtils.settings();
43+
// settings.jackson2Modules.add(SealedClassesModule.class);
44+
// final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from(Shape.class));
45+
// Assertions.assertTrue(output.contains("circle"));
46+
// }
47+
48+
// @Test
49+
// public void testSealedClassWithoutModule() throws JsonProcessingException {
50+
// final Settings settings = TestUtils.settings();
51+
// final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from(Shape.class));
52+
// Assertions.assertFalse(output.contains("circle"));
53+
// }
54+
55+
// @JsonTypeInfo(use = Id.NAME, property = "type")
56+
// private static abstract sealed class Shape {
57+
// }
58+
59+
// @JsonTypeName("circle")
60+
// private static final class Circle extends Shape {
61+
// public double radius;
62+
// }
63+
64+
// public static class SealedClassesModule extends SimpleModule {
65+
// @Override
66+
// public void setupModule(SetupContext context) {
67+
// context.appendAnnotationIntrospector(new SealedClassesAnnotationIntrospector());
68+
// }
69+
// }
70+
71+
// public static class SealedClassesAnnotationIntrospector extends JacksonAnnotationIntrospector {
72+
// @Override
73+
// public List<NamedType> findSubtypes(Annotated a) {
74+
// if (a.getAnnotated() instanceof Class<?> cls && cls.isSealed()) {
75+
// final Class<?>[] permittedSubclasses = cls.getPermittedSubclasses();
76+
// if (permittedSubclasses.length > 0) {
77+
// return Arrays.stream(permittedSubclasses).map(NamedType::new).toList();
78+
// }
79+
// }
80+
// return null;
81+
// }
82+
// }
83+
84+
}

0 commit comments

Comments
 (0)