Skip to content

Commit 5f6d3d3

Browse files
Merge pull request #183 from vojtechhabarta/optional-properties
Optional properties using Jackson2
2 parents 230f749 + 5e7cfb4 commit 5f6d3d3

File tree

9 files changed

+181
-22
lines changed

9 files changed

+181
-22
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
2+
package cz.habarta.typescript.generator;
3+
4+
5+
public enum OptionalProperties {
6+
useSpecifiedAnnotations, useLibraryDefinition, all
7+
}

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

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11

22
package cz.habarta.typescript.generator;
33

4+
import com.fasterxml.jackson.databind.Module;
45
import cz.habarta.typescript.generator.emitter.Emitter;
56
import cz.habarta.typescript.generator.emitter.EmitterExtension;
67
import cz.habarta.typescript.generator.emitter.EmitterExtensionFeatures;
@@ -30,7 +31,8 @@ public class Settings {
3031
public String umdNamespace = null;
3132
public JsonLibrary jsonLibrary = null;
3233
private Predicate<String> excludeFilter = null;
33-
public boolean declarePropertiesAsOptional = false;
34+
@Deprecated public boolean declarePropertiesAsOptional = false;
35+
public OptionalProperties optionalProperties; // default is OptionalProperties.useSpecifiedAnnotations
3436
public boolean declarePropertiesAsReadOnly = false;
3537
public String removeTypeNamePrefix = null;
3638
public String removeTypeNameSuffix = null;
@@ -69,7 +71,9 @@ public class Settings {
6971
public Map<String, String> npmPackageDependencies = new LinkedHashMap<>();
7072
public String typescriptVersion = "^2.4";
7173
public boolean displaySerializerWarning = true;
72-
public boolean disableJackson2ModuleDiscovery = false;
74+
@Deprecated public boolean disableJackson2ModuleDiscovery = false;
75+
public boolean jackson2ModuleDiscovery = false;
76+
public List<Class<? extends Module>> jackson2Modules = new ArrayList<>();
7377
public ClassLoader classLoader = null;
7478

7579
private boolean defaultStringEnumsOverriddenByExtension = false;
@@ -123,6 +127,12 @@ public void loadOptionalAnnotations(ClassLoader classLoader, List<String> option
123127
}
124128
}
125129

130+
public void loadJackson2Modules(ClassLoader classLoader, List<String> jackson2Modules) {
131+
if (jackson2Modules != null) {
132+
this.jackson2Modules = loadClasses(classLoader, jackson2Modules, Module.class);
133+
}
134+
}
135+
126136
public static Map<String, String> convertToMap(List<String> mappings) {
127137
final Map<String, String> result = new LinkedHashMap<>();
128138
if (mappings != null) {
@@ -235,6 +245,16 @@ public void validate() {
235245
throw new RuntimeException("'npmName' and 'npmVersion' is only applicable when generating NPM 'package.json'.");
236246
}
237247
}
248+
249+
if (declarePropertiesAsOptional) {
250+
System.out.println("Warning: Parameter 'declarePropertiesAsOptional' is deprecated. Use 'optionalProperties' parameter.");
251+
if (optionalProperties == null) {
252+
optionalProperties = OptionalProperties.all;
253+
}
254+
}
255+
if (disableJackson2ModuleDiscovery) {
256+
System.out.println("Warning: Parameter 'disableJackson2ModuleDiscovery' was removed. See 'jackson2ModuleDiscovery' and 'jackson2Modules' parameters.");
257+
}
238258
}
239259

240260
private static void reportConfigurationChange(String extensionName, String parameterName, String parameterValue) {

typescript-generator-core/src/main/java/cz/habarta/typescript/generator/emitter/Emitter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ private void emitProperty(TsPropertyModel property) {
187187
emitComments(property.getComments());
188188
final TsType tsType = property.getTsType();
189189
final String readonly = property.readonly ? "readonly " : "";
190-
final String questionMark = settings.declarePropertiesAsOptional || (tsType instanceof TsType.OptionalType) ? "?" : "";
190+
final String questionMark = tsType instanceof TsType.OptionalType ? "?" : "";
191191
writeIndentedLine(readonly + quoteIfNeeded(property.getName(), settings) + questionMark + ": " + tsType.format(settings) + ";");
192192
}
193193

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

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import cz.habarta.typescript.generator.*;
55
import java.lang.annotation.Annotation;
6+
import java.lang.reflect.AnnotatedElement;
67
import java.lang.reflect.Member;
78
import java.lang.reflect.Type;
89
import java.util.*;
@@ -48,6 +49,8 @@ private BeanModel parseBean(SourceType<Class<?>> sourceClass) {
4849
final BeanHelper beanHelper = getBeanHelper(sourceClass.type);
4950
if (beanHelper != null) {
5051
for (BeanPropertyWriter beanPropertyWriter : beanHelper.getProperties()) {
52+
final Member propertyMember = beanPropertyWriter.getMember().getMember();
53+
checkMember(propertyMember, beanPropertyWriter.getName(), sourceClass.type);
5154
Type propertyType = beanPropertyWriter.getGenericPropertyType();
5255
if (propertyType == JsonNode.class) {
5356
propertyType = Object.class;
@@ -65,13 +68,7 @@ private BeanModel parseBean(SourceType<Class<?>> sourceClass) {
6568
continue;
6669
}
6770
}
68-
boolean optional = false;
69-
for (Class<? extends Annotation> optionalAnnotation : settings.optionalAnnotations) {
70-
if (beanPropertyWriter.getAnnotation(optionalAnnotation) != null) {
71-
optional = true;
72-
break;
73-
}
74-
}
71+
final boolean optional = isAnnotatedPropertyOptional((AnnotatedElement) propertyMember);
7572
final Member originalMember = beanPropertyWriter.getMember().getMember();
7673
properties.add(processTypeAndCreateProperty(beanPropertyWriter.getName(), propertyType, optional, sourceClass.type, originalMember, null));
7774
}

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

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.beans.PropertyDescriptor;
2121
import java.lang.annotation.Annotation;
2222
import java.lang.reflect.AccessibleObject;
23+
import java.lang.reflect.AnnotatedElement;
2324
import java.lang.reflect.Field;
2425
import java.lang.reflect.Member;
2526
import java.lang.reflect.Method;
@@ -38,9 +39,16 @@ public Jackson2Parser(Settings settings, TypeProcessor typeProcessor) {
3839

3940
public Jackson2Parser(Settings settings, TypeProcessor typeProcessor, boolean useJaxbAnnotations) {
4041
super(settings, typeProcessor);
41-
if (!settings.disableJackson2ModuleDiscovery) {
42+
if (settings.jackson2ModuleDiscovery) {
4243
objectMapper.registerModules(ObjectMapper.findModules(settings.classLoader));
4344
}
45+
for (Class<? extends Module> moduleClass : settings.jackson2Modules) {
46+
try {
47+
objectMapper.registerModule(moduleClass.newInstance());
48+
} catch (ReflectiveOperationException e) {
49+
throw new RuntimeException(String.format("Cannot instantiate Jackson2 module '%s'", moduleClass.getName()), e);
50+
}
51+
}
4452
if (useJaxbAnnotations) {
4553
AnnotationIntrospector introspector = new JaxbAnnotationIntrospector(objectMapper.getTypeFactory());
4654
objectMapper.setAnnotationIntrospector(introspector);
@@ -63,6 +71,7 @@ private BeanModel parseBean(SourceType<Class<?>> sourceClass) {
6371
if (beanHelper != null) {
6472
for (BeanPropertyWriter beanPropertyWriter : beanHelper.getProperties()) {
6573
final Member propertyMember = beanPropertyWriter.getMember().getMember();
74+
checkMember(propertyMember, beanPropertyWriter.getName(), sourceClass.type);
6675
Type propertyType = getGenericType(propertyMember);
6776
if (propertyType == JsonNode.class) {
6877
propertyType = Object.class;
@@ -80,13 +89,9 @@ private BeanModel parseBean(SourceType<Class<?>> sourceClass) {
8089
continue;
8190
}
8291
}
83-
boolean optional = false;
84-
for (Class<? extends Annotation> optionalAnnotation : settings.optionalAnnotations) {
85-
if (beanPropertyWriter.getAnnotation(optionalAnnotation) != null) {
86-
optional = true;
87-
break;
88-
}
89-
}
92+
final boolean optional = settings.optionalProperties == OptionalProperties.useLibraryDefinition
93+
? !beanPropertyWriter.isRequired()
94+
: isAnnotatedPropertyOptional((AnnotatedElement) propertyMember);
9095
// @JsonUnwrapped
9196
PropertyModel.PullProperties pullProperties = null;
9297
final Member originalMember = beanPropertyWriter.getMember().getMember();

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
import cz.habarta.typescript.generator.compiler.EnumKind;
66
import cz.habarta.typescript.generator.compiler.SymbolTable;
77
import cz.habarta.typescript.generator.*;
8+
import java.lang.annotation.Annotation;
9+
import java.lang.reflect.AnnotatedElement;
10+
import java.lang.reflect.Field;
811
import java.lang.reflect.Member;
12+
import java.lang.reflect.Method;
913
import java.lang.reflect.Type;
1014
import java.util.*;
1115

@@ -79,6 +83,30 @@ private Model parseQueue() {
7983

8084
protected abstract DeclarationModel parseClass(SourceType<Class<?>> sourceClass);
8185

86+
protected static void checkMember(Member propertyMember, String propertyName, Class<?> sourceClass) {
87+
if (!(propertyMember instanceof Field) && !(propertyMember instanceof Method)) {
88+
throw new RuntimeException(String.format(
89+
"Unexpected member type '%s' in property '%s' in class '%s'",
90+
propertyMember != null ? propertyMember.getClass().getName() : null,
91+
propertyName,
92+
sourceClass.getName()));
93+
}
94+
}
95+
96+
protected boolean isAnnotatedPropertyOptional(AnnotatedElement annotatedProperty) {
97+
if (settings.optionalProperties == OptionalProperties.all) {
98+
return true;
99+
}
100+
if (settings.optionalProperties == null || settings.optionalProperties == OptionalProperties.useSpecifiedAnnotations) {
101+
for (Class<? extends Annotation> optionalAnnotation : settings.optionalAnnotations) {
102+
if (annotatedProperty.getAnnotation(optionalAnnotation) != null) {
103+
return true;
104+
}
105+
}
106+
}
107+
return false;
108+
}
109+
82110
protected static DeclarationModel parseEnum(SourceType<Class<?>> sourceClass) {
83111
final List<EnumMemberModel> values = new ArrayList<>();
84112
if (sourceClass.type.isEnum()) {

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

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.fasterxml.jackson.annotation.*;
66
import com.fasterxml.jackson.core.JsonProcessingException;
77
import com.fasterxml.jackson.databind.ObjectMapper;
8+
import javax.xml.bind.annotation.XmlElement;
89
import org.junit.Assert;
910
import org.junit.Test;
1011

@@ -109,4 +110,68 @@ public static void main(String[] args) throws JsonProcessingException {
109110
System.out.println(new ObjectMapper().writeValueAsString(new SubTypeDiscriminatedByName4()));
110111
}
111112

113+
@Test
114+
public void testOptionalJsonProperty() {
115+
final Settings settings = TestUtils.settings();
116+
settings.optionalProperties = OptionalProperties.useLibraryDefinition;
117+
final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from(ClassWithOptionals.class));
118+
Assert.assertTrue(output.contains("oname1?: string"));
119+
// Assert.assertTrue(output.contains("oname2?: string")); // uncomment on Java 8
120+
Assert.assertTrue(output.contains("jname1?: string"));
121+
Assert.assertTrue(output.contains("jname2?: string"));
122+
Assert.assertTrue(output.contains("jname3: string"));
123+
Assert.assertTrue(output.contains("jname4: string"));
124+
Assert.assertTrue(output.contains("xname1?: string"));
125+
Assert.assertTrue(output.contains("xname2?: string"));
126+
Assert.assertTrue(output.contains("xname3?: string"));
127+
Assert.assertTrue(output.contains("xname4?: string"));
128+
}
129+
130+
@Test
131+
public void testOptionalXmlElement() {
132+
final Settings settings = TestUtils.settings();
133+
settings.jsonLibrary = JsonLibrary.jaxb;
134+
settings.optionalProperties = OptionalProperties.useLibraryDefinition;
135+
final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from(ClassWithOptionals.class));
136+
Assert.assertTrue(output.contains("oname1?: string"));
137+
// Assert.assertTrue(output.contains("oname2?: string")); // uncomment on Java 8
138+
Assert.assertTrue(output.contains("jname1?: string"));
139+
Assert.assertTrue(output.contains("jname2?: string"));
140+
Assert.assertTrue(output.contains("jname3?: string"));
141+
Assert.assertTrue(output.contains("jname4?: string"));
142+
Assert.assertTrue(output.contains("xname1?: string"));
143+
Assert.assertTrue(output.contains("xname2?: string"));
144+
Assert.assertTrue(output.contains("xname3: string"));
145+
Assert.assertTrue(output.contains("xname4: string"));
146+
}
147+
148+
public static class ClassWithOptionals {
149+
public String oname1;
150+
// public Optional<String> oname2; // uncomment on Java 8
151+
152+
@JsonProperty
153+
public String jname1;
154+
@JsonProperty(required = false)
155+
public String jname2;
156+
@JsonProperty(required = true)
157+
public String jname3;
158+
private String jname4;
159+
@JsonProperty(required = true)
160+
public String getJname4() {
161+
return jname4;
162+
}
163+
164+
@XmlElement
165+
public String xname1;
166+
@XmlElement(required = false)
167+
public String xname2;
168+
@XmlElement(required = true)
169+
public String xname3;
170+
private String xname4;
171+
@XmlElement(required = true)
172+
public String getXname4() {
173+
return xname4;
174+
}
175+
}
176+
112177
}

typescript-generator-gradle-plugin/src/main/java/cz/habarta/typescript/generator/gradle/GenerateTask.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ public class GenerateTask extends DefaultTask {
2727
public List<String> excludeClassPatterns;
2828
public List<String> includePropertyAnnotations;
2929
public JsonLibrary jsonLibrary;
30-
public boolean declarePropertiesAsOptional;
30+
@Deprecated public boolean declarePropertiesAsOptional;
31+
public OptionalProperties optionalProperties;
3132
public boolean declarePropertiesAsReadOnly;
3233
public String removeTypeNamePrefix;
3334
public String removeTypeNameSuffix;
@@ -62,7 +63,9 @@ public class GenerateTask extends DefaultTask {
6263
public String npmVersion;
6364
public StringQuotes stringQuotes;
6465
public boolean displaySerializerWarning = true;
65-
public boolean disableJackson2ModuleDiscovery;
66+
@Deprecated public boolean disableJackson2ModuleDiscovery;
67+
public boolean jackson2ModuleDiscovery;
68+
public List<String> jackson2Modules;
6669
public boolean debug;
6770

6871
@TaskAction
@@ -101,6 +104,7 @@ public void generate() throws Exception {
101104
settings.setExcludeFilter(excludeClasses, excludeClassPatterns);
102105
settings.jsonLibrary = jsonLibrary;
103106
settings.declarePropertiesAsOptional = declarePropertiesAsOptional;
107+
settings.optionalProperties = optionalProperties;
104108
settings.declarePropertiesAsReadOnly = declarePropertiesAsReadOnly;
105109
settings.removeTypeNamePrefix = removeTypeNamePrefix;
106110
settings.removeTypeNameSuffix = removeTypeNameSuffix;
@@ -137,6 +141,8 @@ public void generate() throws Exception {
137141
settings.setStringQuotes(stringQuotes);
138142
settings.displaySerializerWarning = displaySerializerWarning;
139143
settings.disableJackson2ModuleDiscovery = disableJackson2ModuleDiscovery;
144+
settings.jackson2ModuleDiscovery = jackson2ModuleDiscovery;
145+
settings.loadJackson2Modules(classLoader, jackson2Modules);
140146
settings.classLoader = classLoader;
141147
final File output = outputFile != null
142148
? getProject().file(outputFile)

typescript-generator-maven-plugin/src/main/java/cz/habarta/typescript/generator/maven/GenerateMojo.java

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,11 +131,26 @@ public class GenerateMojo extends AbstractMojo {
131131
private JsonLibrary jsonLibrary;
132132

133133
/**
134-
* If true declared properties will be optional.
134+
* Deprecated, use <code>optionalProperties</code> parameter.
135135
*/
136+
@Deprecated
136137
@Parameter
137138
private boolean declarePropertiesAsOptional;
138139

140+
/**
141+
* Specifies how properties are defined to be optional.
142+
* Supported values are:
143+
* <ul>
144+
* <li><code>useSpecifiedAnnotations</code> - annotations specified using <code>optionalAnnotations</code> parameter</li>
145+
* <li><code>useLibraryDefinition</code> - examples: <code>@JsonProperty(required = false)</code> when using <code>jackson2</code> library
146+
* or <code>@XmlElement(required = false)</code> when using <code>jaxb</code> library</li>
147+
* <li><code>all</code> - all properties are optional</li>
148+
* </ul>
149+
* Default value is <code>useSpecifiedAnnotations</code>.
150+
*/
151+
@Parameter
152+
private OptionalProperties optionalProperties;
153+
139154
/**
140155
* If true declared properties will be <code>readonly</code>.
141156
*/
@@ -404,11 +419,24 @@ public class GenerateMojo extends AbstractMojo {
404419
private boolean displaySerializerWarning;
405420

406421
/**
407-
* Turns off Jackson2 automatic module discovery.
422+
* Deprecated, see <code>jackson2ModuleDiscovery</code> and <code>jackson2Modules</code> parameters.
408423
*/
424+
@Deprecated
409425
@Parameter
410426
private boolean disableJackson2ModuleDiscovery;
411427

428+
/**
429+
* Turns on Jackson2 automatic module discovery.
430+
*/
431+
@Parameter
432+
private boolean jackson2ModuleDiscovery;
433+
434+
/**
435+
* Specifies Jackson2 modules to use.
436+
*/
437+
@Parameter
438+
private List<String> jackson2Modules;
439+
412440
/**
413441
* Turns on verbose output for debugging purposes.
414442
*/
@@ -447,6 +475,7 @@ public void execute() {
447475
settings.setExcludeFilter(excludeClasses, excludeClassPatterns);
448476
settings.jsonLibrary = jsonLibrary;
449477
settings.declarePropertiesAsOptional = declarePropertiesAsOptional;
478+
settings.optionalProperties = optionalProperties;
450479
settings.declarePropertiesAsReadOnly = declarePropertiesAsReadOnly;
451480
settings.removeTypeNamePrefix = removeTypeNamePrefix;
452481
settings.removeTypeNameSuffix = removeTypeNameSuffix;
@@ -483,6 +512,8 @@ public void execute() {
483512
settings.setStringQuotes(stringQuotes);
484513
settings.displaySerializerWarning = displaySerializerWarning;
485514
settings.disableJackson2ModuleDiscovery = disableJackson2ModuleDiscovery;
515+
settings.jackson2ModuleDiscovery = jackson2ModuleDiscovery;
516+
settings.loadJackson2Modules(classLoader, jackson2Modules);
486517
settings.classLoader = classLoader;
487518
final File output = outputFile != null
488519
? outputFile

0 commit comments

Comments
 (0)