Skip to content

Commit 61385a8

Browse files
committed
feature: Support for sealed classes containing tagged unions
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. Sealed types are supported in kotlin 1.6+ and Java 17+, however, we don't really need the sealed functionality, as we are an offline, one-shot generator, this may work to generate the correct type mappings, but fail to parse on the runtime object mapper side unless the user is using the supported kotlin and java versions.
1 parent be26ffb commit 61385a8

File tree

3 files changed

+79
-15
lines changed

3 files changed

+79
-15
lines changed

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

Lines changed: 13 additions & 5 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
}
347347
} else {
348-
taggedUnionClasses = null;
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: 61 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,60 @@ public void testIntermediateUnions() {
689689
Assertions.assertEquals(expected.trim(), output.trim());
690690
}
691691

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

0 commit comments

Comments
 (0)