Skip to content

Commit c542aba

Browse files
authored
fix: additional logic to avoid registration of partial schemas (#2065)
Avoid registration of schemas for reuse when referencing property applies property customization, e.g. `@JsonIgnoreProperties`. Signed-off-by: Michael Edgar <[email protected]>
1 parent 458fb2f commit c542aba

File tree

7 files changed

+239
-99
lines changed

7 files changed

+239
-99
lines changed

core/src/main/java/io/smallrye/openapi/runtime/io/schema/SchemaFactory.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,9 @@ public static Schema readSchema(final AnnotationScannerContext context, Annotati
9393
public static Schema readSchema(final AnnotationScannerContext context,
9494
Schema schema,
9595
AnnotationInstance annotation,
96-
ClassInfo clazz) {
97-
return readSchema(context, schema, annotation, clazz, Collections.emptyMap());
96+
ClassInfo clazz,
97+
boolean registerSchema) {
98+
return readSchema(context, schema, annotation, clazz, registerSchema, Collections.emptyMap());
9899
}
99100

100101
/**
@@ -113,6 +114,7 @@ static Schema readSchema(final AnnotationScannerContext context,
113114
Schema schema,
114115
AnnotationInstance annotation,
115116
ClassInfo clazz,
117+
boolean registerSchema,
116118
Map<String, Object> defaults) {
117119

118120
if (isAnnotationMissingOrHidden(context, annotation, defaults)) {
@@ -129,7 +131,9 @@ static Schema readSchema(final AnnotationScannerContext context,
129131
*
130132
* Ignore the reference returned by register, the caller expects the full schema.
131133
*/
132-
schemaRegistration(context, clazzType, schema);
134+
if (registerSchema) {
135+
schemaRegistration(context, clazzType, schema);
136+
}
133137

134138
return schema;
135139
}
@@ -641,7 +645,7 @@ public static Schema enumToSchema(final AnnotationScannerContext context, Type e
641645
Map<String, Object> defaults = new LinkedHashMap<>(TypeUtil.getTypeAttributes(enumValueType));
642646
defaults.put(SchemaConstant.PROP_ENUMERATION, enumeration);
643647

644-
enumSchema = readSchema(context, enumSchema, schemaAnnotation, enumKlazz, defaults);
648+
enumSchema = readSchema(context, enumSchema, schemaAnnotation, enumKlazz, true, defaults);
645649
} else {
646650
TypeUtil.applyTypeAttributes(enumValueType, enumSchema);
647651
enumSchema.setEnumeration(enumeration);

core/src/main/java/io/smallrye/openapi/runtime/scanner/OpenApiDataObjectScanner.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import io.smallrye.openapi.runtime.scanner.dataobject.AnnotationTargetProcessor;
3535
import io.smallrye.openapi.runtime.scanner.dataobject.AugmentedIndexView;
3636
import io.smallrye.openapi.runtime.scanner.dataobject.DataObjectDeque;
37+
import io.smallrye.openapi.runtime.scanner.dataobject.IgnoreResolver;
3738
import io.smallrye.openapi.runtime.scanner.dataobject.TypeResolver;
3839
import io.smallrye.openapi.runtime.scanner.spi.AnnotationScannerContext;
3940
import io.smallrye.openapi.runtime.util.TypeUtil;
@@ -246,16 +247,19 @@ private void depthFirstGraphSearch() {
246247

247248
ClassInfo currentClass = currentPathEntry.getClazz();
248249
Schema currentSchema = currentPathEntry.getSchema();
250+
AnnotationTarget currentTarget = currentPathEntry.getAnnotationTarget();
251+
boolean allowRegistration = !IgnoreResolver.configuresVisibility(context, currentTarget);
249252

250253
// First, handle class annotations (re-assign since readKlass may return new schema)
251-
currentSchema = readKlass(currentClass, currentType, currentSchema);
254+
currentSchema = readKlass(currentClass, currentType, currentSchema, allowRegistration);
255+
252256
TypeUtil.mapDeprecated(context, currentClass, currentSchema::getDeprecated, currentSchema::setDeprecated);
253257
currentPathEntry.setSchema(currentSchema);
254258

255259
if (!hasNonNullType(currentSchema)) {
256260
// If not schema has yet been set, consider this an "object"
257261
SchemaSupport.setType(currentSchema, Schema.SchemaType.OBJECT);
258-
} else {
262+
} else if (allowRegistration) {
259263
maybeRegisterSchema(currentType, currentSchema, entrySchema);
260264
}
261265

@@ -364,14 +368,15 @@ private void encloseCurrentSchema(Schema currentSchema, Type currentType, DataOb
364368

365369
private Schema readKlass(ClassInfo currentClass,
366370
Type currentType,
367-
Schema currentSchema) {
371+
Schema currentSchema,
372+
boolean registerSchema) {
368373

369374
AnnotationInstance annotation = TypeUtil.getSchemaAnnotation(context, currentClass);
370375
Schema classSchema;
371376

372377
if (annotation != null) {
373378
// Because of implementation= field, *may* return a new schema rather than modify.
374-
classSchema = SchemaFactory.readSchema(context, currentSchema, annotation, currentClass);
379+
classSchema = SchemaFactory.readSchema(context, currentSchema, annotation, currentClass, registerSchema);
375380
} else if (isA(currentType, ENUM_TYPE)) {
376381
classSchema = SchemaFactory.enumToSchema(context, currentType);
377382
} else {

core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/IgnoreResolver.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,40 @@ public Visibility referenceVisibility(String propertyName,
7878
return Visibility.UNSET;
7979
}
8080

81+
/**
82+
* Check whether the given target contains annotations that configure the
83+
* visibility of the properties of the type annotated.
84+
*
85+
* This is used to only allow registration of a type if the annotation
86+
* target (field or method) that refers to the type's class is not annotated
87+
* with an annotation that alters the visibility of fields in the class.
88+
*
89+
* <p>
90+
* For example, in this scenario when we process `fieldB`, registration of
91+
* class B's schema will not occur because it's definition is altered by and
92+
* specific to its use in class A.
93+
*
94+
* <pre>
95+
* <code>
96+
* class A {
97+
* &#64;JsonIgnoreProperties({"field2"})
98+
* B fieldB;
99+
* }
100+
*
101+
* class B {
102+
* int field1;
103+
* int field2;
104+
* }
105+
* </code>
106+
* </pre>
107+
*/
108+
public static boolean configuresVisibility(AnnotationScannerContext context, AnnotationTarget target) {
109+
if (target != null) {
110+
return context.getIgnoreResolver().configuresVisibility(target);
111+
}
112+
return false;
113+
}
114+
81115
public boolean configuresVisibility(AnnotationTarget reference) {
82116
for (IgnoreAnnotationHandler handler : ignoreHandlers) {
83117
if (handler.configuresVisibility(reference)) {

core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/TypeProcessor.java

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -155,27 +155,10 @@ public Type processType() {
155155
* refers to the type's class is not annotated with an annotation that alters
156156
* the visibility of fields in the class.
157157
*
158-
* <p>
159-
* For example, in this scenario when we process `fieldB`, registration of class B's
160-
* schema will not occur because it's definition is altered by and specific to its
161-
* use in class A.
162-
*
163-
* <pre>
164-
* <code>
165-
* class A {
166-
* &#64;JsonIgnoreProperties({"field2"})
167-
* B fieldB;
168-
* }
169-
*
170-
* class B {
171-
* int field1;
172-
* int field2;
173-
* }
174-
* </code>
175-
* </pre>
158+
* @see IgnoreResolver#configuresVisibility(AnnotationScannerContext, AnnotationTarget)
176159
*/
177160
public boolean allowRegistration() {
178-
return !context.getIgnoreResolver().configuresVisibility(annotationTarget);
161+
return !IgnoreResolver.configuresVisibility(context, annotationTarget);
179162
}
180163

181164
private Type readArrayType(ArrayType arrayType, Schema arraySchema) {

core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/TypeResolver.java

Lines changed: 61 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@
4141
import io.smallrye.openapi.api.constants.JaxbConstants;
4242
import io.smallrye.openapi.api.constants.JsonbConstants;
4343
import io.smallrye.openapi.runtime.io.schema.SchemaConstant;
44-
import io.smallrye.openapi.runtime.scanner.dataobject.IgnoreResolver.Visibility;
4544
import io.smallrye.openapi.runtime.scanner.spi.AnnotationScannerContext;
4645
import io.smallrye.openapi.runtime.util.Annotations;
4746
import io.smallrye.openapi.runtime.util.JandexUtil;
@@ -581,47 +580,6 @@ private static boolean isNonPublicOrAbsent(MethodInfo method) {
581580
return method == null || !Modifier.isPublic(method.flags());
582581
}
583582

584-
private static boolean isViewable(AnnotationScannerContext context, AnnotationTarget propertySource) {
585-
Map<Type, Boolean> activeViews = context.getJsonViews();
586-
587-
if (activeViews.isEmpty()) {
588-
return true;
589-
}
590-
591-
Type[] applicableViews = getJsonViews(context, propertySource);
592-
593-
if (applicableViews != null && applicableViews.length > 0) {
594-
return Arrays.stream(applicableViews).anyMatch(activeViews::containsKey);
595-
}
596-
597-
return true;
598-
}
599-
600-
/**
601-
* Find {@literal @}JsonView annotations on the field/method, or on the
602-
* declaring class when the field/method itself it not directly targeted by the
603-
* annotation.
604-
*/
605-
private static Type[] getJsonViews(AnnotationScannerContext context, AnnotationTarget propertySource) {
606-
Annotations annotations = context.annotations();
607-
608-
Type[] directViews = annotations.getAnnotationValue(propertySource, JacksonConstants.JSON_VIEW);
609-
610-
if (directViews != null) {
611-
return directViews;
612-
}
613-
614-
ClassInfo declaringClass;
615-
616-
if (propertySource.kind() == Kind.FIELD) {
617-
declaringClass = propertySource.asField().declaringClass();
618-
} else {
619-
declaringClass = propertySource.asMethod().declaringClass();
620-
}
621-
622-
return annotations.getAnnotationValue(declaringClass, JacksonConstants.JSON_VIEW);
623-
}
624-
625583
/**
626584
* Determine if the target should be exposed in the API or ignored. Explicitly un-hiding a property
627585
* via the <code>@Schema</code> annotation overrides configuration to ignore the same property
@@ -638,12 +596,6 @@ private void processVisibility(AnnotationScannerContext context, AnnotationTarge
638596
return;
639597
}
640598

641-
if (this.isUnhidden(target, reference)) {
642-
// @Schema with hidden = false and not already ignored by a member lower in the class hierarchy
643-
this.exposed = true;
644-
return;
645-
}
646-
647599
switch (getVisibility(context, target, reference, descendants, propertyName)) {
648600
case EXPOSED:
649601
this.exposed = true;
@@ -706,13 +658,26 @@ private IgnoreResolver.Visibility getVisibility(AnnotationScannerContext context
706658
List<ClassInfo> descendants,
707659
String propertyName) {
708660

661+
IgnoreResolver.Visibility visibility;
662+
709663
if (!isViewable(context, target)) {
710664
return IgnoreResolver.Visibility.IGNORED;
711665
}
712666

713667
IgnoreResolver ignoreResolver = context.getIgnoreResolver();
668+
visibility = ignoreResolver.referenceVisibility(propertyName, target, reference);
669+
670+
if (visibility != IgnoreResolver.Visibility.UNSET) {
671+
return visibility;
672+
}
673+
674+
if (isUnhidden(target)) {
675+
// @Schema with hidden = false and not already ignored by a member lower in the class hierarchy
676+
return IgnoreResolver.Visibility.EXPOSED;
677+
}
678+
714679
// First check if a descendant class has hidden/exposed the property
715-
IgnoreResolver.Visibility visibility = ignoreResolver.getDescendantVisibility(propertyName, descendants);
680+
visibility = ignoreResolver.getDescendantVisibility(propertyName, descendants);
716681

717682
if (visibility == IgnoreResolver.Visibility.UNSET) {
718683
visibility = ignoreResolver.isIgnore(propertyName, target, reference);
@@ -721,23 +686,64 @@ private IgnoreResolver.Visibility getVisibility(AnnotationScannerContext context
721686
return visibility;
722687
}
723688

689+
private static boolean isViewable(AnnotationScannerContext context, AnnotationTarget propertySource) {
690+
Map<Type, Boolean> activeViews = context.getJsonViews();
691+
692+
if (activeViews.isEmpty()) {
693+
return true;
694+
}
695+
696+
Type[] applicableViews = getJsonViews(context, propertySource);
697+
698+
if (applicableViews != null && applicableViews.length > 0) {
699+
return Arrays.stream(applicableViews).anyMatch(activeViews::containsKey);
700+
}
701+
702+
return true;
703+
}
704+
705+
/**
706+
* Find {@literal @}JsonView annotations on the field/method, or on the
707+
* declaring class when the field/method itself it not directly targeted by the
708+
* annotation.
709+
*/
710+
private static Type[] getJsonViews(AnnotationScannerContext context, AnnotationTarget propertySource) {
711+
Annotations annotations = context.annotations();
712+
713+
AnnotationInstance jsonView = annotations.getAnnotation(propertySource, JacksonConstants.JSON_VIEW);
714+
715+
if (jsonView != null) {
716+
return annotations.value(jsonView);
717+
}
718+
719+
ClassInfo declaringClass;
720+
721+
if (propertySource.kind() == Kind.FIELD) {
722+
declaringClass = propertySource.asField().declaringClass();
723+
} else {
724+
declaringClass = propertySource.asMethod().declaringClass();
725+
}
726+
727+
return annotations.getAnnotationValue(declaringClass, JacksonConstants.JSON_VIEW);
728+
}
729+
724730
/**
725731
* Determines whether the target is set to hidden = false via the <code>@Schema</code>
726732
* annotation (explicit or default value). A field is only considered un-hidden if its
727733
* visibility is not set to ignored by the referencing annotation target.
728734
*
729735
* @return true if the field is un-hidden, false otherwise
730736
*/
731-
boolean isUnhidden(AnnotationTarget target, AnnotationTarget reference) {
737+
boolean isUnhidden(AnnotationTarget target) {
732738
if (target != null) {
733-
AnnotationInstance schemaAnnotation = TypeUtil.getSchemaAnnotation(context, target);
739+
Annotations annotations = context.annotations();
740+
AnnotationInstance schema = annotations.getAnnotation(target, SchemaConstant.DOTNAME_SCHEMA);
734741

735-
if (schemaAnnotation != null) {
736-
Boolean hidden = context.annotations().value(schemaAnnotation, SchemaConstant.PROP_HIDDEN);
742+
if (schema != null) {
743+
Boolean hidden = annotations.value(schema, SchemaConstant.PROP_HIDDEN);
737744

738745
if (hidden == null || !hidden.booleanValue()) {
739-
return context.getIgnoreResolver().referenceVisibility(propertyName, target,
740-
reference) != Visibility.IGNORED;
746+
return true;
741747
}
742748
}
743749
}

extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/JsonViewTests.java

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -136,10 +136,6 @@ class Role {
136136
private UUID id;
137137
@JsonView(Views.Ingest.class)
138138
private String name;
139-
@JsonView(Views.Full.class)
140-
private LocalDateTime createdAt;
141-
// Adding @Schema() here does fix the problem were @JsonIgnoreProperties in Group was removing description gobally.
142-
// but now the @JsonView is being ignored and description field is in all JsonViews.
143139
@Schema(title = "Title of description")
144140
@JsonView(Views.Full.class)
145141
private String description;
@@ -152,13 +148,10 @@ class Group {
152148
@JsonView(Views.Abridged.class)
153149
private String name;
154150
@JsonView(Views.Full.class)
155-
private LocalDateTime createdAt;
156-
@JsonView(Views.Full.class)
157151
private String description;
158152
@JsonView(Views.Ingest.class)
159153
private String roleId;
160154
@JsonView(Views.Abridged.class)
161-
// @JsonIgnoreProperties should only apply to this the Group entity use case of Role. Currently, it's global.
162155
@JsonIgnoreProperties("description")
163156
private List<Role> roles;
164157
}
@@ -199,8 +192,41 @@ public Response post(@RequestBody @JsonView(Views.Ingest.class) User group) {
199192
}
200193
}
201194

195+
@Path("/role")
196+
class RoleResource {
197+
@GET
198+
@Produces(MediaType.APPLICATION_JSON)
199+
@JsonView(Views.Full.class)
200+
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = Role.class)))
201+
public Response getRole() {
202+
return null;
203+
}
204+
205+
@POST
206+
public Response post(@RequestBody @JsonView(Views.Ingest.class) Role role) {
207+
return null;
208+
}
209+
}
210+
211+
@Path("/group")
212+
class GroupResource {
213+
@GET
214+
@Produces(MediaType.APPLICATION_JSON)
215+
@JsonView(Views.Full.class)
216+
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = Group.class)))
217+
public Response get() {
218+
return null;
219+
}
220+
221+
@POST
222+
public Response post(@RequestBody @JsonView(Views.Ingest.class) Group group) {
223+
return null;
224+
}
225+
}
226+
202227
Index index = Index.of(Views.class, Views.Max.class, Views.Full.class, Views.Ingest.class, Views.Abridged.class,
203-
User.class, Group.class, Role.class, UserResource.class, LocalDateTime.class);
228+
User.class, Group.class, Role.class, UserResource.class, LocalDateTime.class, RoleResource.class,
229+
GroupResource.class);
204230
OpenApiConfig config = dynamicConfig(SmallRyeOASConfig.SMALLRYE_REMOVE_UNUSED_SCHEMAS, Boolean.TRUE);
205231
OpenApiAnnotationScanner scanner = new OpenApiAnnotationScanner(config, index);
206232

0 commit comments

Comments
 (0)