Skip to content
This repository was archived by the owner on Dec 19, 2023. It is now read-only.

Commit b553c5c

Browse files
authored
Handle not null Kotlin properties and Java Optional / nullable Kotlin parameters (1.0.x) (#231)
* Handle not null Kotlin properties and Java Optional / nullable Kotlin parameters (#230) * Detect required Java fields and not-null and thus required Kotlin properties * Detect Optional and Kotlin nullables in parameters * Fix comments * Remove tests that depend on Java 8
1 parent 0df34bb commit b553c5c

File tree

10 files changed

+83
-19
lines changed

10 files changed

+83
-19
lines changed

spring-auto-restdocs-core/src/main/java/capital/scalable/restdocs/jackson/FieldDocumentationObjectVisitor.java

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,25 @@ public FieldDocumentationObjectVisitor(SerializerProvider provider,
5151
this.typeFactory = typeFactory;
5252
}
5353

54+
/**
55+
* Called for required bean properties like fields/properties annotated with @JsonProperty(required = true)
56+
* or non-null Kotlin properties.
57+
*/
5458
@Override
5559
public void property(BeanProperty prop) throws JsonMappingException {
56-
optionalProperty(prop);
60+
property(prop, true);
5761
}
5862

63+
/**
64+
* Called for all non-required bean properties like all not annotated Java fields
65+
* or not annotated nullable Kotlin properties.
66+
*/
5967
@Override
6068
public void optionalProperty(BeanProperty prop) throws JsonMappingException {
69+
property(prop, false);
70+
}
71+
72+
public void property(BeanProperty prop, boolean required) throws JsonMappingException {
6173
String jsonName = prop.getName();
6274
String fieldName = prop.getMember().getName();
6375

@@ -71,19 +83,19 @@ public void optionalProperty(BeanProperty prop) throws JsonMappingException {
7183
return;
7284
}
7385

74-
visitType(prop, jsonName, fieldName, javaType, ser);
86+
visitType(prop, jsonName, fieldName, javaType, ser, required);
7587
}
7688
}
7789

7890
private void visitType(BeanProperty prop, String jsonName, String fieldName, JavaType fieldType,
79-
JsonSerializer<?> ser) throws JsonMappingException {
91+
JsonSerializer<?> ser, boolean required) throws JsonMappingException {
8092
String fieldPath = path + (path.isEmpty() ? "" : ".") + jsonName;
8193
log.debug("({}) {}", fieldPath, fieldType.getRawClass().getSimpleName());
8294
Class<?> javaBaseClass = prop.getMember().getDeclaringClass();
8395
boolean shouldExpand = shouldExpand(prop);
8496

8597
InternalFieldInfo fieldInfo = new InternalFieldInfo(javaBaseClass, fieldName, fieldType,
86-
fieldPath, shouldExpand);
98+
fieldPath, shouldExpand, required);
8799

88100
JsonFormatVisitorWrapper visitor = new FieldDocumentationVisitorWrapper(getProvider(),
89101
context, fieldPath, fieldInfo, typeRegistry, typeFactory);

spring-auto-restdocs-core/src/main/java/capital/scalable/restdocs/jackson/FieldDocumentationVisitorContext.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ public void addField(InternalFieldInfo info, String jsonType) {
8484
.description(comment);
8585

8686
Attribute constraints = constraintAttribute(javaBaseClass, javaFieldName);
87-
Attribute optionals = optionalAttribute(javaBaseClass, javaFieldName);
87+
Attribute optionals = optionalAttribute(javaBaseClass, javaFieldName, info.isRequired());
8888
Attribute deprecated = deprecatedAttribute(javaBaseClass, javaFieldName);
8989
fieldDescriptor.attributes(constraints, optionals, deprecated);
9090

@@ -110,19 +110,24 @@ private Attribute constraintAttribute(Class<?> javaBaseClass, String javaFieldNa
110110
resolveConstraintDescriptions(javaBaseClass, javaFieldName));
111111
}
112112

113-
private Attribute optionalAttribute(Class<?> javaBaseClass, String javaFieldName) {
113+
private Attribute optionalAttribute(Class<?> javaBaseClass, String javaFieldName, boolean requiredField) {
114114
return new Attribute(OPTIONAL_ATTRIBUTE,
115-
resolveOptionalMessages(javaBaseClass, javaFieldName));
115+
resolveOptionalMessages(javaBaseClass, javaFieldName, requiredField));
116116
}
117117

118118
private Attribute deprecatedAttribute(Class<?> javaBaseClass, String javaFieldName) {
119119
return new Attribute(DEPRECATED_ATTRIBUTE,
120120
resolveDeprecatedMessage(javaBaseClass, javaFieldName));
121121
}
122122

123-
private List<String> resolveOptionalMessages(Class<?> javaBaseClass, String javaFieldName) {
123+
private List<String> resolveOptionalMessages(Class<?> javaBaseClass, String javaFieldName, boolean requiredField) {
124124
List<String> optionalMessages = new ArrayList<>();
125125

126+
if (requiredField) {
127+
optionalMessages.add("false");
128+
return optionalMessages;
129+
}
130+
126131
if (isPrimitive(javaBaseClass, javaFieldName)) {
127132
boolean failOnNullForPrimitives = deserializationConfig.hasDeserializationFeatures(
128133
DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES.getMask());

spring-auto-restdocs-core/src/main/java/capital/scalable/restdocs/jackson/InternalFieldInfo.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,20 @@ class InternalFieldInfo {
2727
private final JavaType javaFieldType;
2828
private final String jsonFieldPath;
2929
private final boolean shouldExpand;
30+
/**
31+
* True for required bean properties like fields annotated with @JsonProperty(required = true)
32+
* or non-null properties in Kotlin.
33+
*/
34+
private final boolean required;
3035

3136
public InternalFieldInfo(Class<?> javaBaseClass, String javaFieldName, JavaType javaFieldType,
32-
String jsonFieldPath, boolean shouldExpand) {
37+
String jsonFieldPath, boolean shouldExpand, boolean required) {
3338
this.javaBaseClass = javaBaseClass;
3439
this.javaFieldName = javaFieldName;
3540
this.javaFieldType = javaFieldType;
3641
this.jsonFieldPath = jsonFieldPath;
3742
this.shouldExpand = shouldExpand;
43+
this.required = required;
3844
}
3945

4046
public Class<?> getJavaBaseClass() {
@@ -56,4 +62,8 @@ public String getJsonFieldPath() {
5662
public boolean shouldExpand() {
5763
return shouldExpand;
5864
}
65+
66+
public boolean isRequired() {
67+
return required;
68+
}
5969
}

spring-auto-restdocs-core/src/main/java/capital/scalable/restdocs/request/AbstractParameterSnippet.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ private void addFieldDescriptor(HandlerMethod handlerMethod,
8484
String pathName = getPath(annot);
8585

8686
String parameterName = hasLength(pathName) ? pathName : javaParameterName;
87-
String parameterTypeName = determineTypeName(param.getParameterType());
87+
String parameterTypeName = determineTypeName(param.nestedIfOptional().getNestedParameterType());
8888
String description = javadocReader.resolveMethodParameterComment(
8989
handlerMethod.getBeanType(), handlerMethod.getMethod().getName(),
9090
javaParameterName);

spring-auto-restdocs-core/src/main/java/capital/scalable/restdocs/request/PathParametersSnippet.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,10 @@ public PathParametersSnippet failOnUndocumentedParams(boolean failOnUndocumented
4242

4343
@Override
4444
protected boolean isRequired(MethodParameter param, PathVariable annot) {
45-
// Spring disallows null for primitive types
46-
return param.getParameterType().isPrimitive() || annot.required();
45+
// Spring disallows null for primitive types.
46+
// For types wrapped in Optional or nullable Kotlin types, the required flag in
47+
// the annotation is ignored by Spring.
48+
return param.getParameterType().isPrimitive() || (!param.isOptional() && annot.required());
4749
}
4850

4951
@Override

spring-auto-restdocs-core/src/main/java/capital/scalable/restdocs/request/RequestHeaderSnippet.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ public RequestHeaderSnippet failOnUndocumentedParams(boolean failOnUndocumentedP
4343
@Override
4444
protected boolean isRequired(MethodParameter param, RequestHeader annot) {
4545
// Spring disallows null for primitive types
46-
return param.getParameterType().isPrimitive() || annot.required();
46+
// For types wrapped in Optional or nullable Kotlin types, the required flag in
47+
// the annotation is ignored by Spring.
48+
return param.getParameterType().isPrimitive() || (!param.isOptional() && annot.required());
4749
}
4850

4951
@Override

spring-auto-restdocs-core/src/main/java/capital/scalable/restdocs/request/RequestParametersSnippet.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,9 @@ protected boolean isRequired(MethodParameter param, RequestParam annot) {
5757
// the required flag
5858
return true;
5959
} else {
60-
return annot.required();
60+
// For Types wrapped in Optional or nullable Kotlin types, the required flag in
61+
// the annotation is ignored by Spring.
62+
return !param.isOptional() && annot.required();
6163
}
6264
}
6365

spring-auto-restdocs-core/src/test/java/capital/scalable/restdocs/jackson/FieldDocumentationGeneratorTest.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,27 @@ public void testGenerateDocumentationWithTags() throws Exception {
458458
"true")));
459459
}
460460

461+
@Test
462+
public void testGenerateDocumentationForRequiredProperties() throws Exception {
463+
// given
464+
ObjectMapper mapper = createMapper();
465+
mockFieldComment(RequiredProperties.class, "string", "A string");
466+
mockFieldComment(RequiredProperties.class, "number", "An integer");
467+
468+
FieldDocumentationGenerator generator =
469+
new FieldDocumentationGenerator(mapper.writer(), mapper.getDeserializationConfig(),
470+
javadocReader, constraintReader);
471+
Type type = RequiredProperties.class;
472+
473+
// when
474+
List<ExtendedFieldDescriptor> result = cast(generator
475+
.generateDocumentation(type, mapper.getTypeFactory()));
476+
// then
477+
assertThat(result.size(), is(2));
478+
assertThat(result.get(0), is(descriptor("string", "String", "A string", "false")));
479+
assertThat(result.get(1), is(descriptor("number", "Integer", "An integer", "false")));
480+
}
481+
461482
private OngoingStubbing<List<String>> mockOptional(Class<?> javaBaseClass, String fieldName,
462483
String value) {
463484
return when(constraintReader.getOptionalMessages(javaBaseClass, fieldName))
@@ -695,4 +716,11 @@ private static class JsonType2SubType1 extends JsonType2 {
695716
private static class JsonType2SubType2 extends JsonType2 {
696717
private String base2Sub2;
697718
}
719+
720+
private static class RequiredProperties {
721+
@JsonProperty(required = true)
722+
private String string;
723+
@JsonProperty(required = true)
724+
private Integer number;
725+
}
698726
}

spring-auto-restdocs-core/src/test/java/capital/scalable/restdocs/request/PathParametersSnippetTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ enum Shape {
185185
SPHERIC, SQUARE
186186
}
187187

188-
@PostMapping("/items/{id}/subitem/{subid}/{partId}/{yetAnotherId}")
188+
@PostMapping("/items/{id}/subitem/{subid}/{partId}/{yetAnotherId}/{optionalId}")
189189
public void addItem(@PathVariable Integer id,
190190
@PathVariable("subid") String otherId,
191191
// partId is required anyway, because it's a primitive type

spring-auto-restdocs-core/src/test/java/capital/scalable/restdocs/request/RequestParametersSnippetTest.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -303,19 +303,22 @@ public enum Size {
303303
SMALL, LARGE
304304
}
305305

306-
public void searchItem(@RequestParam Integer type,
306+
public void searchItem(
307+
@RequestParam Integer type,
307308
@RequestParam(value = "text", required = false) String description) {
308309
// NOOP
309310
}
310311

311-
public void searchItem2(@RequestParam double param1, // required
312+
public void searchItem2(
313+
@RequestParam double param1, // required
312314
@RequestParam(required = false) boolean param2, // required anyway
313315
@RequestParam(defaultValue = "1") int param3) { // not required
314316
// NOOP
315317
}
316318

317-
public void searchItem2String(@RequestParam double param1, // required
318-
@RequestParam(required = false) boolean param2, // required anyway
319+
public void searchItem2String(
320+
@RequestParam double param1, // required
321+
@RequestParam(required = false) boolean param2, // required anyway
319322
@RequestParam(defaultValue = "de") String param3) { // not required
320323
// NOOP
321324
}

0 commit comments

Comments
 (0)