Skip to content

Commit eb6a794

Browse files
committed
Fix process jackson annotations together with swagger annotations
1 parent a011b63 commit eb6a794

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;
@@ -118,6 +117,7 @@
118117
import java.util.OptionalLong;
119118
import java.util.Set;
120119
import java.util.UUID;
120+
import java.util.concurrent.atomic.AtomicReference;
121121
import java.util.function.BiConsumer;
122122
import java.util.function.Function;
123123

@@ -148,6 +148,7 @@
148148
import static io.micronaut.openapi.visitor.OpenApiApplicationVisitor.resolvePlaceholders;
149149
import static io.micronaut.openapi.visitor.OpenApiConfigProperty.MICRONAUT_OPENAPI_FIELD_VISIBILITY_LEVEL;
150150
import static io.micronaut.openapi.visitor.OpenApiModelProp.DISCRIMINATOR;
151+
import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_ACCESS;
151152
import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_ACCESS_MODE;
152153
import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_ADDITIONAL_PROPERTIES;
153154
import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_ALLOWABLE_VALUES;
@@ -358,7 +359,8 @@ public static Schema<?> getSchemaDefinition(OpenAPI openAPI,
358359
if (type instanceof EnumElement enumEl) {
359360
schema = setSpecVersion(new Schema<>());
360361
schema.setName(schemaName);
361-
if (javadoc != null && StringUtils.hasText(javadoc.getMethodDescription())) {
362+
processJacksonDescription(enumEl, schema);
363+
if (schema.getDescription() == null && javadoc != null && StringUtils.hasText(javadoc.getMethodDescription())) {
362364
schema.setDescription(javadoc.getMethodDescription());
363365
}
364366
schemas.put(schemaName, schema);
@@ -374,7 +376,8 @@ public static Schema<?> getSchemaDefinition(OpenAPI openAPI,
374376
if (schemaWithSuperTypes != null) {
375377
schema = schemaWithSuperTypes;
376378
}
377-
if (schema != null && javadoc != null && StringUtils.hasText(javadoc.getMethodDescription())) {
379+
processJacksonDescription(type, schema);
380+
if (schema != null && schema.getDescription() == null && javadoc != null && StringUtils.hasText(javadoc.getMethodDescription())) {
378381
schema.setDescription(javadoc.getMethodDescription());
379382
}
380383

@@ -434,7 +437,7 @@ public static Schema<?> getSchemaDefinition(OpenAPI openAPI,
434437
if (externalDocs != null) {
435438
schema.setExternalDocs(externalDocs);
436439
}
437-
setSchemaDocumentation(type, schema);
440+
setSchemaDescription(type, schema);
438441
var schemaRef = setSpecVersion(new Schema<>());
439442
schemaRef.set$ref(SchemaUtils.schemaRef(schema.getName()));
440443
if (definingElement instanceof ClassElement classEl && classEl.isIterable()) {
@@ -840,6 +843,8 @@ public static Schema<?> resolveSchema(OpenAPI openApi, @Nullable Element definin
840843
processSchemaAnn(schema, context, definingElement, type, schemaAnnotationValue);
841844
}
842845

846+
processJacksonDescription(definingElement, schema);
847+
843848
if (definingElement != null && StringUtils.isEmpty(schema.getDescription())) {
844849
if (fieldJavadoc != null) {
845850
if (StringUtils.hasText(fieldJavadoc.getMethodDescription())) {
@@ -930,7 +935,7 @@ public static Schema<?> bindSchemaForElement(VisitorContext context, TypedElemen
930935
}
931936

932937
boolean notOnlyRef = false;
933-
setSchemaDocumentation(element, topLevelSchema);
938+
setSchemaDescription(element, topLevelSchema);
934939
if (StringUtils.isNotEmpty(topLevelSchema.getDescription())) {
935940
notOnlyRef = true;
936941
}
@@ -954,11 +959,14 @@ && isProtobufGenerated(propertyEl.getOwningType())
954959
SchemaUtils.setNullable(topLevelSchema);
955960
notOnlyRef = true;
956961
}
957-
final String defaultJacksonValue = stringValue(element, JsonProperty.class, PROP_DEFAULT_VALUE).orElse(null);
958-
if (defaultJacksonValue != null && schemaToBind.getDefault() == null) {
959-
setDefaultValueObject(topLevelSchema, defaultJacksonValue, elementType, schemaToBind.getType(), schemaToBind.getFormat(), false, context);
962+
if (processJacksonPropertyAnn(element, elementType, topLevelSchema, schemaAnn, context)) {
960963
notOnlyRef = true;
961964
}
965+
// final String defaultJacksonValue = stringValue(element, JsonProperty.class, PROP_DEFAULT_VALUE).orElse(null);
966+
// if (defaultJacksonValue != null && schemaToBind.getDefault() == null) {
967+
// setDefaultValueObject(topLevelSchema, defaultJacksonValue, elementType, schemaToBind.getType(), schemaToBind.getFormat(), false, context);
968+
// notOnlyRef = true;
969+
// }
962970

963971
boolean addSchemaToBind = !SchemaUtils.isEmptySchema(schemaToBind);
964972

@@ -1728,37 +1736,59 @@ private static void checkAllOf(Schema<Object> composedSchema) {
17281736
composedSchema.addAllOfItem(propSchema);
17291737
}
17301738

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

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

1789+
if (schemaAnn == null) {
1790+
return;
1791+
}
17621792
Map<CharSequence, Object> annValues = schemaAnn.getValues();
17631793
if (annValues.containsKey(PROP_NAME)) {
17641794
schemaToBind.setName((String) annValues.get(PROP_NAME));
@@ -2133,6 +2163,59 @@ private static void processArgTypeAnnotations(ClassElement type, @Nullable Schem
21332163
processJakartaValidationAnnotations(type, type, schema);
21342164
}
21352165

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

21382221
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)