Skip to content

Commit 22b4fa6

Browse files
Objects serialized as references using IDs (#298)
- `JsonIdentityInfo` and `JsonIdentityReference` annotations
1 parent b765f2f commit 22b4fa6

File tree

5 files changed

+192
-4
lines changed

5 files changed

+192
-4
lines changed

typescript-generator-core/src/main/java/cz/habarta/typescript/generator/DefaultTypeProcessor.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
package cz.habarta.typescript.generator;
33

44
import cz.habarta.typescript.generator.parser.JaxrsApplicationParser;
5+
import cz.habarta.typescript.generator.util.UnionType;
56
import cz.habarta.typescript.generator.util.Utils;
67
import java.lang.reflect.*;
78
import java.math.*;
89
import java.util.*;
10+
import java.util.stream.Collectors;
911
import javax.xml.bind.JAXBElement;
1012

1113

@@ -116,6 +118,20 @@ public Result processType(Type javaType, Context context) {
116118
? context.processType(upperBounds[0])
117119
: new Result(TsType.Any);
118120
}
121+
if (javaType instanceof UnionType) {
122+
final UnionType unionType = (UnionType) javaType;
123+
final List<Result> results = unionType.types.stream()
124+
.map(type -> context.processType(type))
125+
.collect(Collectors.toList());
126+
return new Result(
127+
new TsType.UnionType(results.stream()
128+
.map(result -> result.getTsType())
129+
.collect(Collectors.toList())),
130+
results.stream()
131+
.flatMap(result -> result.getDiscoveredClasses().stream())
132+
.collect(Collectors.toList())
133+
);
134+
}
119135
return null;
120136
}
121137

typescript-generator-core/src/main/java/cz/habarta/typescript/generator/TsType.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,10 @@ public static class UnionType extends TsType {
184184

185185
public final List<TsType> types;
186186

187+
public UnionType(TsType... types) {
188+
this(Arrays.asList(types));
189+
}
190+
187191
public UnionType(List<? extends TsType> types) {
188192
this.types = new ArrayList<TsType>(new LinkedHashSet<TsType>(types));
189193
}

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

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33

44
import com.fasterxml.jackson.annotation.JsonAutoDetect;
55
import com.fasterxml.jackson.annotation.JsonFormat;
6+
import com.fasterxml.jackson.annotation.JsonIdentityInfo;
7+
import com.fasterxml.jackson.annotation.JsonIdentityReference;
68
import com.fasterxml.jackson.annotation.JsonProperty;
79
import com.fasterxml.jackson.annotation.JsonSubTypes;
810
import com.fasterxml.jackson.annotation.JsonTypeInfo;
911
import com.fasterxml.jackson.annotation.JsonTypeName;
1012
import com.fasterxml.jackson.annotation.JsonUnwrapped;
1113
import com.fasterxml.jackson.annotation.JsonValue;
14+
import com.fasterxml.jackson.annotation.ObjectIdGenerators;
1215
import com.fasterxml.jackson.annotation.PropertyAccessor;
1316
import com.fasterxml.jackson.databind.AnnotationIntrospector;
1417
import com.fasterxml.jackson.databind.BeanDescription;
@@ -32,6 +35,7 @@
3235
import cz.habarta.typescript.generator.compiler.EnumKind;
3336
import cz.habarta.typescript.generator.compiler.EnumMemberModel;
3437
import cz.habarta.typescript.generator.util.Predicate;
38+
import cz.habarta.typescript.generator.util.UnionType;
3539
import cz.habarta.typescript.generator.util.Utils;
3640
import java.beans.BeanInfo;
3741
import java.beans.Introspector;
@@ -46,7 +50,9 @@
4650
import java.util.Arrays;
4751
import java.util.List;
4852
import java.util.Map;
53+
import java.util.Optional;
4954
import java.util.stream.Collectors;
55+
import java.util.stream.Stream;
5056

5157

5258
public class Jackson2Parser extends ModelParser {
@@ -120,6 +126,7 @@ private BeanModel parseBean(SourceType<Class<?>> sourceClass) {
120126
checkMember(propertyMember, beanPropertyWriter.getName(), sourceClass.type);
121127
Type propertyType = getGenericType(propertyMember);
122128

129+
// Map.Entry
123130
final Class<?> propertyRawClass = Utils.getRawClassOrNull(propertyType);
124131
if (propertyRawClass != null && Map.Entry.class.isAssignableFrom(propertyRawClass)) {
125132
final BeanDescription propertyDescription = objectMapper.getSerializationConfig().introspect(beanPropertyWriter.getType());
@@ -130,6 +137,9 @@ private BeanModel parseBean(SourceType<Class<?>> sourceClass) {
130137
propertyType = Utils.replaceRawClassInType(propertyType, Map.class);
131138
}
132139
}
140+
141+
// @JsonIdentityInfo and @JsonIdentityReference
142+
propertyType = processIdentity(propertyType);
133143

134144
if (!isAnnotatedPropertyIncluded(beanPropertyWriter::getAnnotation, sourceClass.type.getName() + "." + beanPropertyWriter.getName())) {
135145
continue;
@@ -191,6 +201,49 @@ private BeanModel parseBean(SourceType<Class<?>> sourceClass) {
191201
return new BeanModel(sourceClass.type, superclass, taggedUnionClasses, discriminantProperty, discriminantLiteral, interfaces, properties, null);
192202
}
193203

204+
private Type processIdentity(Type propertyType) {
205+
final Class<?> cls = Utils.getRawClassOrNull(propertyType);
206+
if (cls != null) {
207+
final JsonIdentityInfo identityInfo = cls.getAnnotation(JsonIdentityInfo.class);
208+
if (identityInfo == null) {
209+
return propertyType;
210+
}
211+
final JsonIdentityReference identityReference = cls.getAnnotation(JsonIdentityReference.class);
212+
final boolean alwaysAsId = identityReference != null && identityReference.alwaysAsId();
213+
214+
final Type idType;
215+
if (identityInfo.generator() == ObjectIdGenerators.None.class) {
216+
return propertyType;
217+
} else if (identityInfo.generator() == ObjectIdGenerators.PropertyGenerator.class) {
218+
final BeanPropertyWriter[] properties = getBeanHelper(cls).getProperties();
219+
final Optional<BeanPropertyWriter> property = Stream.of(properties)
220+
.filter(p -> p.getName().equals(identityInfo.property()))
221+
.findFirst();
222+
if (property.isPresent()) {
223+
final BeanPropertyWriter beanPropertyWriter = property.get();
224+
final Member propertyMember = beanPropertyWriter.getMember().getMember();
225+
checkMember(propertyMember, beanPropertyWriter.getName(), cls);
226+
idType = getGenericType(propertyMember);
227+
} else {
228+
return propertyType;
229+
}
230+
} else if (identityInfo.generator() == ObjectIdGenerators.IntSequenceGenerator.class) {
231+
idType = Integer.class;
232+
} else if (identityInfo.generator() == ObjectIdGenerators.UUIDGenerator.class) {
233+
idType = String.class;
234+
} else if (identityInfo.generator() == ObjectIdGenerators.StringIdGenerator.class) {
235+
idType = String.class;
236+
} else {
237+
idType = Object.class;
238+
}
239+
return alwaysAsId
240+
? idType
241+
: new UnionType(propertyType, idType);
242+
243+
}
244+
return propertyType;
245+
}
246+
194247
private static Type getGenericType(Member member) {
195248
if (member instanceof Method) {
196249
return ((Method) member).getGenericReturnType();
@@ -378,7 +431,7 @@ private DeclarationModel parseEnumOrObjectEnum(SourceType<Class<?>> sourceClass)
378431
return new EnumModel(sourceClass.type, isNumberBased ? EnumKind.NumberBased : EnumKind.StringBased, enumMembers, null);
379432
}
380433

381-
private Number getNumberEnumValue(Field field, Method valueMethod, int index) throws Exception {
434+
private static Number getNumberEnumValue(Field field, Method valueMethod, int index) throws Exception {
382435
if (valueMethod != null) {
383436
final Object valueObject = invokeJsonValueMethod(field, valueMethod);
384437
if (valueObject instanceof Number) {
@@ -388,7 +441,7 @@ private Number getNumberEnumValue(Field field, Method valueMethod, int index) th
388441
return index;
389442
}
390443

391-
private Object getMethodEnumValue(Field field, Method valueMethod) throws Exception {
444+
private static Object getMethodEnumValue(Field field, Method valueMethod) throws Exception {
392445
if (valueMethod != null) {
393446
final Object valueObject = invokeJsonValueMethod(field, valueMethod);
394447
if (valueObject instanceof String || valueObject instanceof Number) {
@@ -404,15 +457,15 @@ private Object getMethodEnumValue(Field field, Method valueMethod) throws Except
404457
return field.getName();
405458
}
406459

407-
private Object invokeJsonValueMethod(Field field, Method valueMethod) throws ReflectiveOperationException {
460+
private static Object invokeJsonValueMethod(Field field, Method valueMethod) throws ReflectiveOperationException {
408461
field.setAccessible(true);
409462
final Object constant = field.get(null);
410463
valueMethod.setAccessible(true);
411464
final Object valueObject = valueMethod.invoke(constant);
412465
return valueObject;
413466
}
414467

415-
private Object getFieldJsonValue(Field field, Field jsonValueField) throws ReflectiveOperationException {
468+
private static Object getFieldJsonValue(Field field, Field jsonValueField) throws ReflectiveOperationException {
416469
field.setAccessible(true);
417470
final Object constant = field.get(null);
418471
jsonValueField.setAccessible(true);
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
2+
package cz.habarta.typescript.generator.util;
3+
4+
import java.lang.reflect.Type;
5+
import java.util.Arrays;
6+
import java.util.List;
7+
8+
9+
public class UnionType implements Type {
10+
11+
public final List<Type> types;
12+
13+
public UnionType(Type... types) {
14+
this(Arrays.asList(types));
15+
}
16+
17+
public UnionType(List<Type> types) {
18+
this.types = types;
19+
}
20+
21+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
2+
package cz.habarta.typescript.generator;
3+
4+
import com.fasterxml.jackson.annotation.JsonIdentityInfo;
5+
import com.fasterxml.jackson.annotation.JsonIdentityReference;
6+
import com.fasterxml.jackson.annotation.JsonProperty;
7+
import com.fasterxml.jackson.annotation.ObjectIdGenerators;
8+
import com.fasterxml.jackson.core.JsonProcessingException;
9+
import com.fasterxml.jackson.databind.ObjectMapper;
10+
import com.fasterxml.jackson.databind.SerializationFeature;
11+
import cz.habarta.typescript.generator.util.Utils;
12+
import org.junit.Assert;
13+
import org.junit.Test;
14+
15+
16+
public class ObjectAsIdTest {
17+
18+
@Test
19+
public void testJackson() throws JsonProcessingException {
20+
final TestObjectA testObjectA = new TestObjectA();
21+
final TestObjectB testObjectB = new TestObjectB();
22+
final TestObjectC<String> testObjectC = new TestObjectC<>("valueC");
23+
final Wrapper wrapper = new Wrapper();
24+
wrapper.testObjectA1 = testObjectA;
25+
wrapper.testObjectA2 = testObjectA;
26+
wrapper.testObjectB1 = testObjectB;
27+
wrapper.testObjectB2 = testObjectB;
28+
wrapper.testObjectC1 = testObjectC;
29+
wrapper.testObjectC2 = testObjectC;
30+
final ObjectMapper objectMapper = Utils.getObjectMapper();
31+
objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
32+
final String json = objectMapper.writeValueAsString(wrapper);
33+
Assert.assertTrue(json.contains("\"testObjectA1\": \"id1\""));
34+
Assert.assertTrue(json.contains("\"testObjectA2\": \"id1\""));
35+
Assert.assertTrue(json.contains("\"testObjectB1\": {"));
36+
Assert.assertTrue(json.contains("\"testObjectB2\": \"id2\""));
37+
Assert.assertTrue(json.contains("\"testObjectC1\": {"));
38+
Assert.assertTrue(json.contains("\"testObjectC2\": \"id2\""));
39+
}
40+
41+
@Test
42+
public void test() {
43+
final Settings settings = TestUtils.settings();
44+
final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from(Wrapper.class));
45+
Assert.assertTrue(output.contains("testObjectA1: string"));
46+
Assert.assertTrue(output.contains("testObjectB1: TestObjectB | string"));
47+
Assert.assertTrue(output.contains("testObjectC1: TestObjectC<string> | string"));
48+
Assert.assertTrue(!output.contains("interface TestObjectA"));
49+
Assert.assertTrue(output.contains("interface TestObjectB"));
50+
Assert.assertTrue(output.contains("interface TestObjectC<T>"));
51+
}
52+
53+
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "@@@id")
54+
@JsonIdentityReference(alwaysAsId = true)
55+
private static class TestObjectA {
56+
57+
@JsonProperty("@@@id")
58+
public String myIdentification = "id1";
59+
60+
public String myProperty = "valueA";
61+
}
62+
63+
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "@@@id")
64+
private static class TestObjectB {
65+
66+
@JsonProperty("@@@id")
67+
public String myIdentification = "id2";
68+
69+
public String myProperty = "valueB";
70+
}
71+
72+
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "@@@id")
73+
private static class TestObjectC<T> {
74+
75+
@JsonProperty("@@@id")
76+
public String myIdentification = "id2";
77+
78+
public T myProperty;
79+
80+
public TestObjectC(T myProperty) {
81+
this.myProperty = myProperty;
82+
}
83+
}
84+
85+
private static class Wrapper {
86+
public TestObjectA testObjectA1;
87+
public TestObjectA testObjectA2;
88+
public TestObjectB testObjectB1;
89+
public TestObjectB testObjectB2;
90+
public TestObjectC<String> testObjectC1;
91+
public TestObjectC<String> testObjectC2;
92+
}
93+
94+
}

0 commit comments

Comments
 (0)