Skip to content

Commit de77395

Browse files
committed
feature: Support for sealed classes containing tagged unions
Java language level 17 and above supports isSealed on member types. This uses the proposal of FasterXML/jackson-module-kotlin#239 and the general solution of FasterXML/jackson-module-kotlin#240 to import subtypes of a sealed object or interface that are annotated with `@JsonTypeName` and assignable to the container class or interface to create a much less verbose method of generating tagged unions.
1 parent be26ffb commit de77395

File tree

4 files changed

+90
-16
lines changed

4 files changed

+90
-16
lines changed

typescript-generator-core/pom.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,14 @@
331331
</execution>
332332
</executions>
333333
</plugin>
334+
<plugin>
335+
<groupId>org.apache.maven.plugins</groupId>
336+
<artifactId>maven-compiler-plugin</artifactId>
337+
<configuration>
338+
<source>17</source>
339+
<target>17</target>
340+
</configuration>
341+
</plugin>
334342
</plugins>
335343
</build>
336344

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

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
1010
import com.fasterxml.jackson.annotation.JsonSubTypes;
1111
import com.fasterxml.jackson.annotation.JsonTypeInfo;
12+
import com.fasterxml.jackson.annotation.JsonTypeName;
1213
import com.fasterxml.jackson.annotation.JsonUnwrapped;
1314
import com.fasterxml.jackson.annotation.JsonView;
1415
import com.fasterxml.jackson.annotation.ObjectIdGenerators;
@@ -108,11 +109,11 @@ public Jackson2Parser create(Settings settings, TypeProcessor commonTypeProcesso
108109
}
109110

110111
public static class JaxbParserFactory extends Jackson2ParserFactory {
111-
112+
112113
public JaxbParserFactory() {
113114
super(true);
114115
}
115-
116+
116117
}
117118

118119
private final ObjectMapper objectMapper = new ObjectMapper();
@@ -336,16 +337,23 @@ private BeanModel parseBean(SourceType<Class<?>> sourceClass, List<String> class
336337
}
337338
}
338339

339-
final List<Class<?>> taggedUnionClasses;
340+
final List<Class<?>> taggedUnionClasses = new ArrayList();
340341
final JsonSubTypes jsonSubTypes = sourceClass.type.getAnnotation(JsonSubTypes.class);
341342
if (jsonSubTypes != null) {
342-
taggedUnionClasses = new ArrayList<>();
343343
for (JsonSubTypes.Type type : jsonSubTypes.value()) {
344344
addBeanToQueue(new SourceType<>(type.value(), sourceClass.type, "<subClass>"));
345345
taggedUnionClasses.add(type.value());
346346
}
347-
} else {
348-
taggedUnionClasses = null;
347+
} else if(sourceClass.type.isSealed()){
348+
for (Class<?> cls: sourceClass.type.getDeclaredClasses()){
349+
if(sourceClass.type.isAssignableFrom(cls)){
350+
JsonTypeName jsonTypeName = cls.getAnnotation(JsonTypeName.class);
351+
if(jsonTypeName != null){
352+
addBeanToQueue(new SourceType<>(cls, sourceClass.type, "<subClass>"));
353+
taggedUnionClasses.add(cls);
354+
}
355+
}
356+
}
349357
}
350358
final Type superclass = sourceClass.type.getGenericSuperclass() == Object.class ? null : sourceClass.type.getGenericSuperclass();
351359
if (superclass != null) {

typescript-generator-core/src/test/java/cz/habarta/typescript/generator/Jackson2ParserTest.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,9 @@ public void testTaggedUnion() {
9393
final BeanModel bean4 = model.getBean(SubTypeDiscriminatedByName4.class);
9494
final BeanModel bean5 = model.getBean(SubTypeDiscriminatedByName5.class);
9595
Assertions.assertEquals(4, bean0.getTaggedUnionClasses().size());
96-
Assertions.assertNull(bean1.getTaggedUnionClasses());
97-
Assertions.assertNull(bean2.getTaggedUnionClasses());
98-
Assertions.assertNull(bean3.getTaggedUnionClasses());
96+
Assertions.assertTrue(bean1.getTaggedUnionClasses().isEmpty());
97+
Assertions.assertTrue(bean2.getTaggedUnionClasses().isEmpty());
98+
Assertions.assertTrue(bean3.getTaggedUnionClasses().isEmpty());
9999
Assertions.assertEquals("kind", bean0.getDiscriminantProperty());
100100
Assertions.assertEquals("explicit-name1", bean1.getDiscriminantLiteral());
101101
Assertions.assertEquals("SubType2", bean2.getDiscriminantLiteral());
@@ -492,7 +492,7 @@ private ClassWithJsonCreatorConstructor(@JsonProperty("a") String a, @MyOptional
492492
public String toString() {
493493
return "{" + "a=" + a + ", b=" + b + '}';
494494
}
495-
495+
496496
}
497497
@Test
498498
public void testFactoryMethod() throws JsonProcessingException {
@@ -526,7 +526,7 @@ public static ClassWithJsonCreatorFactoryMethod create(@JsonProperty("a") String
526526
public String toString() {
527527
return "{" + "a=" + a + ", b=" + b + '}';
528528
}
529-
529+
530530
}
531531

532532
@Retention(RetentionPolicy.RUNTIME)

typescript-generator-core/src/test/java/cz/habarta/typescript/generator/TaggedUnionsTest.java

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,21 +82,21 @@ private static class CCircle2 implements IShape2 {
8282
})
8383
interface IShape3 {
8484
}
85-
85+
8686
interface IQuadrilateral3 extends IShape3 {
8787
}
88-
88+
8989
interface INamedShape3 extends IShape3 {
9090
String getName();
9191
}
92-
92+
9393
interface INamedQuadrilateral3 extends INamedShape3, IQuadrilateral3 {
9494
}
95-
95+
9696
@JsonTypeName("rectangle")
9797
interface IRectangle3 extends INamedQuadrilateral3 {
9898
}
99-
99+
100100
@JsonTypeName("circle")
101101
interface ICircle3 extends INamedShape3 {
102102
}
@@ -689,4 +689,62 @@ public void testIntermediateUnions() {
689689
Assertions.assertEquals(expected.trim(), output.trim());
690690
}
691691

692+
693+
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
694+
public sealed interface SealedInterface {
695+
@JsonTypeName("a")
696+
final class SealedInterfaceA implements SealedInterface {
697+
}
698+
@JsonTypeName
699+
final class SealedInterfaceB implements SealedInterface {
700+
}
701+
final class SealedInterfaceC_ShouldBeMissing{}
702+
}
703+
@Test
704+
public void testSealedInterfaceDetection() {
705+
final String output = new TypeScriptGenerator(TestUtils.settings()).generateTypeScript(Input.from(SealedInterface.class));
706+
Assertions.assertEquals("\n" +
707+
"interface SealedInterface {\n" +
708+
" \"@type\": \"TaggedUnionsTest$SealedInterface$SealedInterfaceB\" | \"a\";\n" +
709+
"}\n" +
710+
"\n" +
711+
"interface SealedInterfaceB extends SealedInterface {\n" +
712+
" \"@type\": \"TaggedUnionsTest$SealedInterface$SealedInterfaceB\";\n" +
713+
"}\n" +
714+
"\n" +
715+
"interface SealedInterfaceA extends SealedInterface {\n" +
716+
" \"@type\": \"a\";\n" +
717+
"}\n" +
718+
"\n" +
719+
"type SealedInterfaceUnion = SealedInterfaceB | SealedInterfaceA;\n", output);
720+
}
721+
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
722+
public sealed class SealedClass {
723+
@JsonTypeName("a")
724+
final class SealedClassA extends SealedClass {
725+
}
726+
@JsonTypeName
727+
final class SealedClassB extends SealedClass {
728+
}
729+
final class SealedClassC_ShouldBeMissing{}
730+
}
731+
@Test
732+
public void testSealedClassDetection() {
733+
final String output = new TypeScriptGenerator(TestUtils.settings()).generateTypeScript(Input.from(SealedClass.class));
734+
Assertions.assertEquals("\n" +
735+
"interface SealedClass {\n" +
736+
" \"@type\": \"TaggedUnionsTest$SealedClass\" | \"TaggedUnionsTest$SealedClass$SealedClassB\" | \"a\";\n" +
737+
"}\n" +
738+
"\n" +
739+
"interface SealedClassB extends SealedClass {\n" +
740+
" \"@type\": \"TaggedUnionsTest$SealedClass$SealedClassB\";\n" +
741+
"}\n" +
742+
"\n" +
743+
"interface SealedClassA extends SealedClass {\n" +
744+
" \"@type\": \"a\";\n" +
745+
"}\n" +
746+
"\n" +
747+
"type SealedClassUnion = SealedClassB | SealedClassA;\n", output);
748+
}
749+
692750
}

0 commit comments

Comments
 (0)