Skip to content

Commit c74dd8f

Browse files
committed
Fix process jackson annotations together with swagger annotations
1 parent 16b07b2 commit c74dd8f

8 files changed

Lines changed: 267 additions & 80 deletions

File tree

openapi/src/main/java/io/micronaut/openapi/visitor/OpenApiModelProp.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,6 @@ public interface OpenApiModelProp {
9090
String PROP_FLOWS = "flows";
9191
String PROP_OPEN_ID_CONNECT_URL = "openIdConnectUrl";
9292
String PROP_BEARER_FORMAT = "bearerFormat";
93+
94+
String PROP_ACCESS = "access";
9395
}

openapi/src/main/java/io/micronaut/openapi/visitor/OpenApiNormalizeUtils.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,7 @@ private static void unwrapAllOff(Schema<?> schema) {
522522

523523
for (var entry : innerSchemas.entrySet()) {
524524
var innerSchema = entry.getValue();
525+
innerSchema.setName(null);
525526
if (StringUtils.isNotEmpty(innerSchema.getTitle())) {
526527
schema.setTitle(innerSchema.getTitle());
527528
innerSchema.setTitle(null);
@@ -544,6 +545,22 @@ private static void unwrapAllOff(Schema<?> schema) {
544545
}
545546
innerSchema.setNullable(null);
546547
}
548+
if (innerSchema.getDefault() != null) {
549+
schema.setDefault(innerSchema.getDefault());
550+
innerSchema.setDefault(null);
551+
}
552+
if (innerSchema.getAnyOf() != null && schema.getAnyOf() == null) {
553+
schema.setAnyOf(innerSchema.getAnyOf());
554+
innerSchema.setAnyOf(null);
555+
}
556+
if (innerSchema.getOneOf() != null && schema.getOneOf() == null) {
557+
schema.setOneOf(innerSchema.getOneOf());
558+
innerSchema.setOneOf(null);
559+
}
560+
if (innerSchema.getNot() != null && schema.getNot() == null) {
561+
schema.setNot(innerSchema.getNot());
562+
innerSchema.setNot(null);
563+
}
547564
if (CollectionUtils.isNotEmpty(innerSchema.getRequired())) {
548565
schema.setRequired(innerSchema.getRequired());
549566
innerSchema.setRequired(null);

openapi/src/main/java/io/micronaut/openapi/visitor/SchemaDefinitionUtils.java

Lines changed: 111 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
import io.micronaut.core.annotation.AnnotationClassValue;
3232
import io.micronaut.core.annotation.AnnotationValue;
3333
import io.micronaut.core.annotation.Internal;
34-
import io.micronaut.core.annotation.NonNull;
3534
import io.micronaut.core.annotation.Nullable;
3635
import io.micronaut.core.beans.BeanMap;
3736
import io.micronaut.core.bind.annotation.Bindable;
@@ -117,6 +116,7 @@
117116
import java.util.OptionalLong;
118117
import java.util.Set;
119118
import java.util.UUID;
119+
import java.util.concurrent.atomic.AtomicReference;
120120
import java.util.function.BiConsumer;
121121
import java.util.function.Function;
122122

@@ -147,6 +147,7 @@
147147
import static io.micronaut.openapi.visitor.OpenApiApplicationVisitor.resolvePlaceholders;
148148
import static io.micronaut.openapi.visitor.OpenApiConfigProperty.MICRONAUT_OPENAPI_FIELD_VISIBILITY_LEVEL;
149149
import static io.micronaut.openapi.visitor.OpenApiModelProp.DISCRIMINATOR;
150+
import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_ACCESS;
150151
import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_ACCESS_MODE;
151152
import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_ADDITIONAL_PROPERTIES;
152153
import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_ALLOWABLE_VALUES;
@@ -356,7 +357,8 @@ public static Schema<?> getSchemaDefinition(OpenAPI openAPI,
356357
if (type instanceof EnumElement enumEl) {
357358
schema = setSpecVersion(new Schema<>());
358359
schema.setName(schemaName);
359-
if (javadoc != null && StringUtils.hasText(javadoc.getMethodDescription())) {
360+
processJacksonDescription(enumEl, schema);
361+
if (schema.getDescription() == null && javadoc != null && StringUtils.hasText(javadoc.getMethodDescription())) {
360362
schema.setDescription(javadoc.getMethodDescription());
361363
}
362364
schemas.put(schemaName, schema);
@@ -372,7 +374,8 @@ public static Schema<?> getSchemaDefinition(OpenAPI openAPI,
372374
if (schemaWithSuperTypes != null) {
373375
schema = schemaWithSuperTypes;
374376
}
375-
if (schema != null && javadoc != null && StringUtils.hasText(javadoc.getMethodDescription())) {
377+
processJacksonDescription(type, schema);
378+
if (schema != null && schema.getDescription() == null && javadoc != null && StringUtils.hasText(javadoc.getMethodDescription())) {
376379
schema.setDescription(javadoc.getMethodDescription());
377380
}
378381

@@ -432,7 +435,7 @@ public static Schema<?> getSchemaDefinition(OpenAPI openAPI,
432435
if (externalDocs != null) {
433436
schema.setExternalDocs(externalDocs);
434437
}
435-
setSchemaDocumentation(type, schema);
438+
setSchemaDescription(type, schema);
436439
var schemaRef = setSpecVersion(new Schema<>());
437440
schemaRef.set$ref(SchemaUtils.schemaRef(schema.getName()));
438441
if (definingElement instanceof ClassElement classEl && classEl.isIterable()) {
@@ -838,6 +841,8 @@ public static Schema<?> resolveSchema(OpenAPI openApi, @Nullable Element definin
838841
processSchemaAnn(schema, context, definingElement, type, schemaAnnotationValue);
839842
}
840843

844+
processJacksonDescription(definingElement, schema);
845+
841846
if (definingElement != null && StringUtils.isEmpty(schema.getDescription())) {
842847
if (fieldJavadoc != null) {
843848
if (StringUtils.hasText(fieldJavadoc.getMethodDescription())) {
@@ -928,7 +933,7 @@ public static Schema<?> bindSchemaForElement(VisitorContext context, TypedElemen
928933
}
929934

930935
boolean notOnlyRef = false;
931-
setSchemaDocumentation(element, topLevelSchema);
936+
setSchemaDescription(element, topLevelSchema);
932937
if (StringUtils.isNotEmpty(topLevelSchema.getDescription())) {
933938
notOnlyRef = true;
934939
}
@@ -952,11 +957,14 @@ && isProtobufGenerated(propertyEl.getOwningType())
952957
SchemaUtils.setNullable(topLevelSchema);
953958
notOnlyRef = true;
954959
}
955-
final String defaultJacksonValue = stringValue(element, JsonProperty.class, PROP_DEFAULT_VALUE).orElse(null);
956-
if (defaultJacksonValue != null && schemaToBind.getDefault() == null) {
957-
setDefaultValueObject(topLevelSchema, defaultJacksonValue, elementType, schemaToBind.getType(), schemaToBind.getFormat(), false, context);
960+
if (processJacksonPropertyAnn(element, elementType, topLevelSchema, schemaAnn, context)) {
958961
notOnlyRef = true;
959962
}
963+
// final String defaultJacksonValue = stringValue(element, JsonProperty.class, PROP_DEFAULT_VALUE).orElse(null);
964+
// if (defaultJacksonValue != null && schemaToBind.getDefault() == null) {
965+
// setDefaultValueObject(topLevelSchema, defaultJacksonValue, elementType, schemaToBind.getType(), schemaToBind.getFormat(), false, context);
966+
// notOnlyRef = true;
967+
// }
960968

961969
boolean addSchemaToBind = !SchemaUtils.isEmptySchema(schemaToBind);
962970

@@ -1731,37 +1739,59 @@ private static void checkAllOf(Schema<Object> composedSchema) {
17311739
composedSchema.addAllOfItem(propSchema);
17321740
}
17331741

1734-
private static void setSchemaDocumentation(Element element, Schema<?> schemaToBind) {
1735-
if (StringUtils.isEmpty(schemaToBind.getDescription())) {
1736-
// First, find getter method javadoc
1737-
String doc = element.getDocumentation().orElse(null);
1738-
if (StringUtils.isEmpty(doc)) {
1739-
// next, find field javadoc
1740-
if (element instanceof MemberElement memberEl) {
1741-
List<FieldElement> fields = memberEl.getDeclaringType().getFields();
1742-
if (CollectionUtils.isNotEmpty(fields)) {
1743-
for (FieldElement field : fields) {
1744-
if (field.getName().equals(element.getName())) {
1745-
doc = field.getDocumentation().orElse(null);
1746-
break;
1747-
}
1742+
private static void processJacksonDescription(@Nullable Element element, @Nullable Schema<?> schemaToBind) {
1743+
if (element == null || schemaToBind == null || StringUtils.isNotEmpty(schemaToBind.getDescription())) {
1744+
return;
1745+
}
1746+
findAnnotation(element, element instanceof ClassElement
1747+
? "com.fasterxml.jackson.annotation.JsonClassDescription"
1748+
: "com.fasterxml.jackson.annotation.JsonPropertyDescription"
1749+
)
1750+
.flatMap(ann -> ann.stringValue(PROP_VALUE))
1751+
.ifPresent(schemaToBind::setDescription);
1752+
}
1753+
1754+
private static void setSchemaDescription(Element element, Schema<?> schemaToBind) {
1755+
if (StringUtils.isNotEmpty(schemaToBind.getDescription())) {
1756+
return;
1757+
}
1758+
1759+
processJacksonDescription(element, schemaToBind);
1760+
if (StringUtils.isNotEmpty(schemaToBind.getDescription())) {
1761+
return;
1762+
}
1763+
1764+
// First, find getter method javadoc
1765+
String doc = element.getDocumentation().orElse(null);
1766+
if (StringUtils.isEmpty(doc)) {
1767+
// next, find field javadoc
1768+
if (element instanceof MemberElement memberEl) {
1769+
List<FieldElement> fields = memberEl.getDeclaringType().getFields();
1770+
if (CollectionUtils.isNotEmpty(fields)) {
1771+
for (FieldElement field : fields) {
1772+
if (field.getName().equals(element.getName())) {
1773+
doc = field.getDocumentation().orElse(null);
1774+
break;
17481775
}
17491776
}
17501777
}
17511778
}
1752-
if (doc != null) {
1753-
JavadocDescription desc = Utils.getJavadocParser().parse(doc);
1754-
if (StringUtils.hasText(desc.getMethodDescription())) {
1755-
schemaToBind.setDescription(desc.getMethodDescription());
1756-
}
1779+
}
1780+
if (doc != null) {
1781+
JavadocDescription desc = Utils.getJavadocParser().parse(doc);
1782+
if (StringUtils.hasText(desc.getMethodDescription())) {
1783+
schemaToBind.setDescription(desc.getMethodDescription());
17571784
}
17581785
}
17591786
}
17601787

17611788
private static void processSchemaAnn(Schema schemaToBind, VisitorContext context, Element element,
17621789
@Nullable ClassElement classEl,
1763-
@NonNull AnnotationValue<io.swagger.v3.oas.annotations.media.Schema> schemaAnn) {
1790+
@Nullable AnnotationValue<io.swagger.v3.oas.annotations.media.Schema> schemaAnn) {
17641791

1792+
if (schemaAnn == null) {
1793+
return;
1794+
}
17651795
Map<CharSequence, Object> annValues = schemaAnn.getValues();
17661796
if (annValues.containsKey(PROP_NAME)) {
17671797
schemaToBind.setName((String) annValues.get(PROP_NAME));
@@ -2136,6 +2166,59 @@ private static void processArgTypeAnnotations(ClassElement type, @Nullable Schem
21362166
processJakartaValidationAnnotations(type, type, schema);
21372167
}
21382168

2169+
private static boolean processJacksonPropertyAnn(Element element, ClassElement elType, Schema<?> schemaToBind,
2170+
@Nullable AnnotationValue<io.swagger.v3.oas.annotations.media.Schema> schemaAnn,
2171+
VisitorContext context) {
2172+
2173+
var swaggerAccessMode = schemaAnn != null ? schemaAnn.stringValue(PROP_ACCESS_MODE).orElse(null) : null;
2174+
var swaggerDefaultValue = schemaAnn != null ? schemaAnn.stringValue(PROP_DEFAULT_VALUE).orElse(null) : null;
2175+
2176+
var reference = new AtomicReference<>(false);
2177+
findAnnotation(element, "com.fasterxml.jackson.annotation.JsonProperty")
2178+
.ifPresent(ann -> {
2179+
if (swaggerAccessMode == null) {
2180+
ann.get(PROP_ACCESS, JsonProperty.Access.class).ifPresent(access -> {
2181+
switch (access) {
2182+
case READ_ONLY:
2183+
schemaToBind.setWriteOnly(null);
2184+
schemaToBind.setReadOnly(true);
2185+
reference.set(true);
2186+
break;
2187+
case WRITE_ONLY:
2188+
schemaToBind.setWriteOnly(true);
2189+
schemaToBind.setReadOnly(null);
2190+
reference.set(true);
2191+
break;
2192+
case READ_WRITE:
2193+
schemaToBind.setWriteOnly(null);
2194+
schemaToBind.setReadOnly(null);
2195+
break;
2196+
default:
2197+
break;
2198+
}
2199+
});
2200+
}
2201+
if (swaggerDefaultValue == null) {
2202+
ann.stringValue(PROP_DEFAULT_VALUE).ifPresent(defaultValue -> {
2203+
Pair<String, String> typeAndFormat;
2204+
if (elType.isIterable()) {
2205+
typeAndFormat = Pair.of(TYPE_ARRAY, null);
2206+
} else if (elType instanceof EnumElement enumEl) {
2207+
typeAndFormat = ConvertUtils.checkEnumJsonValueType(context, enumEl, null, null);
2208+
} else {
2209+
typeAndFormat = ConvertUtils.getTypeAndFormatByClass(elType.getName(), elType.isArray());
2210+
}
2211+
setDefaultValueObject(schemaToBind, defaultValue, elType, typeAndFormat.getFirst(), typeAndFormat.getSecond(), false, context);
2212+
if (schemaToBind.getDefault() != null) {
2213+
reference.set(true);
2214+
}
2215+
});
2216+
}
2217+
});
2218+
2219+
return reference.get();
2220+
}
2221+
21392222
private static void processJakartaValidationAnnotations(Element element, ClassElement elementType, Schema<?> schemaToBind) {
21402223

21412224
final boolean isIterableOrMap = elementType.isIterable() || elementType.isAssignable(Map.class);

openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiBasicSchemaSpec.groovy

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1434,14 +1434,14 @@ public class MyBean {}
14341434
schema.properties.uuid.type == 'string'
14351435
schema.properties.uuid.format == 'uuid'
14361436

1437-
// TODO: need to add support custom format for DateTime
14381437
schema.properties.date.default == OffsetDateTime.parse('2007-12-03T10:15:30+01:00')
14391438
schema.properties.date.type == 'string'
14401439
schema.properties.date.format == 'date-time'
14411440

1442-
schema.properties.mySubObject.allOf.get(1).default == 'myDefault3'
1443-
schema.properties.mySubObject.allOf.get(1).type == null
1444-
schema.properties.mySubObject.allOf.get(1).format == null
1441+
schema.properties.mySubObject.default == 'myDefault3'
1442+
!schema.properties.mySubObject.type
1443+
!schema.properties.mySubObject.format
1444+
schema.properties.mySubObject.allOf[0].$ref == "#/components/schemas/MySubObject"
14451445
}
14461446

14471447
@Issue("https://github.com/micronaut-projects/micronaut-openapi/issues/947")

openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiEnumSpec.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1095,9 +1095,9 @@ class MyBean {}
10951095
then:
10961096
operation
10971097
parametersSchema
1098+
parametersSchema.properties.stampAlign.default == 'RIGHT'
10981099
parametersSchema.properties.stampAlign.allOf
10991100
parametersSchema.properties.stampAlign.allOf[0].$ref == '#/components/schemas/ParagraphAlignment'
1100-
parametersSchema.properties.stampAlign.allOf[1].default == 'RIGHT'
11011101
}
11021102

11031103
void "test enum in map key"() {

openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiRecursionSpec.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ class MyBean {}
177177
Schema woopsieRef = testImpl1.properties."woopsie-id"
178178

179179
woopsieRef.description == "woopsie doopsie"
180-
woopsieRef.allOf.size() == 2
180+
woopsieRef.allOf.size() == 1
181181
woopsieRef.allOf[0].$ref == "#/components/schemas/TestInterface"
182182
Schema woopsie = schemas.TestInterface
183183
woopsie

0 commit comments

Comments
 (0)