Skip to content

Commit 5fc5d0e

Browse files
committed
schema resolution options - Phase 2: global allOf
1 parent e924632 commit 5fc5d0e

File tree

17 files changed

+607
-213
lines changed

17 files changed

+607
-213
lines changed

modules/swagger-core/src/main/java/io/swagger/v3/core/converter/ModelConverters.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public ModelConverters(boolean openapi31) {
4545
public ModelConverters(boolean openapi31, Schema.SchemaResolution schemaResolution) {
4646
converters = new CopyOnWriteArrayList<>();
4747
if (openapi31) {
48-
converters.add(new ModelResolver(Json31.mapper()).openapi31(true));
48+
converters.add(new ModelResolver(Json31.mapper()).openapi31(true).schemaResolution(schemaResolution));
4949
} else {
5050
converters.add(new ModelResolver(Json.mapper()).schemaResolution(schemaResolution));
5151
}
@@ -81,7 +81,7 @@ public static ModelConverters getInstance(boolean openapi31, Schema.SchemaResolu
8181
synchronized (ModelConverters.class) {
8282
if (openapi31) {
8383
if (SINGLETON31 == null) {
84-
SINGLETON31 = new ModelConverters(openapi31);
84+
SINGLETON31 = new ModelConverters(openapi31, Schema.SchemaResolution.DEFAULT);
8585
init(SINGLETON31);
8686
}
8787
return SINGLETON31;

modules/swagger-core/src/main/java/io/swagger/v3/core/jackson/ModelResolver.java

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -682,7 +682,7 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context
682682

683683
Annotation[] ctxAnnotation31 = null;
684684

685-
if (openapi31) {
685+
if (Schema.SchemaResolution.ALL_OF.equals(this.schemaResolution) || openapi31) {
686686
List<Annotation> ctxAnnotations31List = new ArrayList<>();
687687
if (annotations != null) {
688688
for (Annotation a : annotations) {
@@ -701,15 +701,18 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context
701701

702702
AnnotatedType aType = new AnnotatedType()
703703
.type(propType)
704-
.ctxAnnotations(openapi31 ? ctxAnnotation31 : annotations)
705704
.parent(model)
706705
.resolveAsRef(annotatedType.isResolveAsRef())
707706
.jsonViewAnnotation(annotatedType.getJsonViewAnnotation())
708707
.skipSchemaName(true)
709708
.schemaProperty(true)
710709
.components(annotatedType.getComponents())
711710
.propertyName(propName);
712-
711+
if (Schema.SchemaResolution.ALL_OF.equals(this.schemaResolution) || openapi31) {
712+
aType.ctxAnnotations(ctxAnnotation31);
713+
} else {
714+
aType.ctxAnnotations(annotations);
715+
}
713716
final AnnotatedMember propMember = member;
714717
aType.jsonUnwrappedHandler(t -> {
715718
JsonUnwrapped uw = propMember.getAnnotation(JsonUnwrapped.class);
@@ -726,6 +729,7 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context
726729
});
727730
property = context.resolve(aType);
728731
property = clone(property);
732+
Schema ctxProperty = null;
729733
if (openapi31) {
730734
Optional<Schema> reResolvedProperty = AnnotationsUtils.getSchemaFromAnnotation(ctxSchema, annotatedType.getComponents(), null, openapi31, property, schemaResolution, context);
731735
if (reResolvedProperty.isPresent()) {
@@ -736,6 +740,16 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context
736740
property = reResolvedProperty.get();
737741
}
738742

743+
} else if (Schema.SchemaResolution.ALL_OF.equals(this.schemaResolution)) {
744+
Optional<Schema> reResolvedProperty = AnnotationsUtils.getSchemaFromAnnotation(ctxSchema, annotatedType.getComponents(), null, openapi31, null, schemaResolution, context);
745+
if (reResolvedProperty.isPresent()) {
746+
ctxProperty = reResolvedProperty.get();
747+
}
748+
reResolvedProperty = AnnotationsUtils.getArraySchema(ctxArraySchema, annotatedType.getComponents(), null, openapi31, ctxProperty);
749+
if (reResolvedProperty.isPresent()) {
750+
ctxProperty = reResolvedProperty.get();
751+
}
752+
739753
}
740754
if (property != null) {
741755
Boolean required = md.getRequired();
@@ -777,10 +791,15 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context
777791
if (context.getDefinedModels().containsKey(pName)) {
778792
if (Schema.SchemaResolution.INLINE.equals(this.schemaResolution)) {
779793
property = context.getDefinedModels().get(pName);
794+
} else if (Schema.SchemaResolution.ALL_OF.equals(this.schemaResolution) && ctxProperty != null) {
795+
property = new Schema()
796+
.addAllOfItem(ctxProperty)
797+
.addAllOfItem(new Schema().$ref(constructRef(pName)));
780798
} else {
781799
property = new Schema().$ref(constructRef(pName));
782800
}
783801
property = clone(property);
802+
// TODO: why is this needed? is it not handled before?
784803
if (openapi31 || Schema.SchemaResolution.INLINE.equals(this.schemaResolution)) {
785804
Optional<Schema> reResolvedProperty = AnnotationsUtils.getSchemaFromAnnotation(ctxSchema, annotatedType.getComponents(), null, openapi31, property, this.schemaResolution, context);
786805
if (reResolvedProperty.isPresent()) {
@@ -794,7 +813,13 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context
794813
}
795814
} else if (property.get$ref() != null) {
796815
if (!openapi31) {
797-
property = new Schema().$ref(StringUtils.isNotEmpty(property.get$ref()) ? property.get$ref() : property.getName());
816+
if (Schema.SchemaResolution.ALL_OF.equals(this.schemaResolution) && ctxProperty != null) {
817+
property = new Schema()
818+
.addAllOfItem(ctxProperty)
819+
.addAllOfItem(new Schema().$ref(StringUtils.isNotEmpty(property.get$ref()) ? property.get$ref() : property.getName()));
820+
} else {
821+
property = new Schema().$ref(StringUtils.isNotEmpty(property.get$ref()) ? property.get$ref() : property.getName());
822+
}
798823
} else {
799824
if (StringUtils.isEmpty(property.get$ref())) {
800825
property.$ref(property.getName());
@@ -807,9 +832,12 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context
807832
if (property != null && io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED.equals(requiredMode)) {
808833
addRequiredItem(model, property.getName());
809834
}
835+
if (ctxProperty == null) {
836+
ctxProperty = property;
837+
}
810838
final boolean applyNotNullAnnotations = io.swagger.v3.oas.annotations.media.Schema.RequiredMode.AUTO.equals(requiredMode);
811839
annotations = addGenericTypeArgumentAnnotationsForOptionalField(propDef, annotations);
812-
applyBeanValidatorAnnotations(propDef, property, annotations, model, applyNotNullAnnotations);
840+
applyBeanValidatorAnnotations(propDef, ctxProperty, annotations, model, applyNotNullAnnotations);
813841

814842
props.add(property);
815843
}

modules/swagger-core/src/main/java/io/swagger/v3/core/util/AnnotationsUtils.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,12 @@ public static Optional<Schema> getSchemaFromAnnotation(
621621
} else {
622622
schemaObject = new Schema();
623623
}
624+
} else if (Schema.SchemaResolution.ALL_OF.equals(schemaResolution)) {
625+
if (existingSchema == null) {
626+
schemaObject = new Schema();
627+
} else {
628+
schemaObject = existingSchema;
629+
}
624630
}
625631
} else {
626632
if (existingSchema == null) {

modules/swagger-core/src/main/java/io/swagger/v3/core/util/ParameterProcessor.java

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,19 @@ public static Parameter applyAnnotations(
4242
String[] methodTypes,
4343
JsonView jsonViewAnnotation,
4444
boolean openapi31) {
45+
return applyAnnotations(parameter, type, annotations, components, classTypes, methodTypes, jsonViewAnnotation, openapi31, null);
46+
}
47+
48+
public static Parameter applyAnnotations(
49+
Parameter parameter,
50+
Type type,
51+
List<Annotation> annotations,
52+
Components components,
53+
String[] classTypes,
54+
String[] methodTypes,
55+
JsonView jsonViewAnnotation,
56+
boolean openapi31,
57+
Schema.SchemaResolution schemaResolution) {
4558

4659
final AnnotationsHelper helper = new AnnotationsHelper(annotations, type);
4760
if (helper.isContext()) {
@@ -59,17 +72,57 @@ public static Parameter applyAnnotations(
5972
if (paramSchemaOrArrayAnnotation != null) {
6073
reworkedAnnotations.add(paramSchemaOrArrayAnnotation);
6174
}
75+
io.swagger.v3.oas.annotations.media.Schema ctxSchema = AnnotationsUtils.getSchemaAnnotation(annotations.toArray(new Annotation[0]));
76+
io.swagger.v3.oas.annotations.media.ArraySchema ctxArraySchema = AnnotationsUtils.getArraySchemaAnnotation(annotations.toArray(new Annotation[0]));
77+
Annotation[] ctxAnnotation31 = null;
78+
79+
if (Schema.SchemaResolution.ALL_OF.equals(schemaResolution)) {
80+
List<Annotation> ctxAnnotations31List = new ArrayList<>();
81+
if (annotations != null) {
82+
for (Annotation a : annotations) {
83+
if (
84+
!(a instanceof io.swagger.v3.oas.annotations.media.Schema) &&
85+
!(a instanceof io.swagger.v3.oas.annotations.media.ArraySchema)) {
86+
ctxAnnotations31List.add(a);
87+
}
88+
}
89+
ctxAnnotation31 = ctxAnnotations31List.toArray(new Annotation[ctxAnnotations31List.size()]);
90+
}
91+
}
6292
AnnotatedType annotatedType = new AnnotatedType()
6393
.type(type)
6494
.resolveAsRef(true)
6595
.skipOverride(true)
66-
.jsonViewAnnotation(jsonViewAnnotation)
67-
.ctxAnnotations(reworkedAnnotations.toArray(new Annotation[reworkedAnnotations.size()]));
96+
.jsonViewAnnotation(jsonViewAnnotation);
97+
98+
if (Schema.SchemaResolution.ALL_OF.equals(schemaResolution)) {
99+
annotatedType.ctxAnnotations(ctxAnnotation31);
100+
} else {
101+
annotatedType.ctxAnnotations(reworkedAnnotations.toArray(new Annotation[reworkedAnnotations.size()]));
102+
}
68103

69-
final ResolvedSchema resolvedSchema = ModelConverters.getInstance(openapi31).resolveAsResolvedSchema(annotatedType);
104+
final ResolvedSchema resolvedSchema = ModelConverters.getInstance(openapi31, schemaResolution).resolveAsResolvedSchema(annotatedType);
70105

71106
if (resolvedSchema.schema != null) {
72-
parameter.setSchema(resolvedSchema.schema);
107+
Schema resSchema = AnnotationsUtils.clone(resolvedSchema.schema, openapi31);
108+
Schema ctxSchemaObject = null;
109+
if (Schema.SchemaResolution.ALL_OF.equals(schemaResolution)) {
110+
Optional<Schema> reResolvedSchema = AnnotationsUtils.getSchemaFromAnnotation(ctxSchema, annotatedType.getComponents(), null, openapi31, null, schemaResolution, null);
111+
if (reResolvedSchema.isPresent()) {
112+
ctxSchemaObject = reResolvedSchema.get();
113+
}
114+
reResolvedSchema = AnnotationsUtils.getArraySchema(ctxArraySchema, annotatedType.getComponents(), null, openapi31, ctxSchemaObject);
115+
if (reResolvedSchema.isPresent()) {
116+
ctxSchemaObject = reResolvedSchema.get();
117+
}
118+
119+
}
120+
if (Schema.SchemaResolution.ALL_OF.equals(schemaResolution) && ctxSchemaObject != null) {
121+
resSchema = new Schema()
122+
.addAllOfItem(ctxSchemaObject)
123+
.addAllOfItem(resolvedSchema.schema);
124+
}
125+
parameter.setSchema(resSchema);
73126
}
74127
resolvedSchema.referencedSchemas.forEach(components::addSchemas);
75128

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
package io.swagger.v3.core.resolving;
2+
3+
import io.swagger.v3.core.converter.AnnotatedType;
4+
import io.swagger.v3.core.converter.ModelConverterContextImpl;
5+
import io.swagger.v3.core.jackson.ModelResolver;
6+
import io.swagger.v3.core.matchers.SerializationMatchers;
7+
import io.swagger.v3.oas.annotations.media.Schema;
8+
import org.testng.annotations.Test;
9+
10+
public class AllofResolvingTest extends SwaggerTestBase {
11+
12+
@Test
13+
public void testAllofResolving() {
14+
15+
final ModelResolver modelResolver = new ModelResolver(mapper()).openapi31(false).schemaResolution(io.swagger.v3.oas.models.media.Schema.SchemaResolution.ALL_OF);
16+
final ModelConverterContextImpl c = new ModelConverterContextImpl(modelResolver);
17+
// ModelConverters c = ModelConverters.getInstance(false, io.swagger.v3.oas.models.media.Schema.SchemaResolution.INLINE);
18+
c.resolve(new AnnotatedType(UserSchema.class));
19+
20+
String expectedYaml = "UserProperty:\n" +
21+
" type: object\n" +
22+
" description: Represents a user-specific property\n" +
23+
" example: User-specific example value\n" +
24+
"UserSchema:\n" +
25+
" type: object\n" +
26+
" properties:\n" +
27+
" propertyOne:\n" +
28+
" allOf:\n" +
29+
" - type: object\n" +
30+
" description: First user schema property\n" +
31+
" nullable: true\n" +
32+
" - $ref: '#/components/schemas/UserProperty'\n" +
33+
" propertyTwo:\n" +
34+
" allOf:\n" +
35+
" - type: object\n" +
36+
" description: Second user schema property\n" +
37+
" example: example value for propertyTwo\n" +
38+
" - $ref: '#/components/schemas/UserProperty'\n" +
39+
" propertyThree:\n" +
40+
" allOf:\n" +
41+
" - type: object\n" +
42+
" description: \"Third user schema property, with example for testing\"\n" +
43+
" example: example value for propertyThree\n" +
44+
" - $ref: '#/components/schemas/UserProperty'\n";
45+
46+
SerializationMatchers.assertEqualsToYaml(c.getDefinedModels(), expectedYaml);
47+
// stringSchemaMap = c.readAll(InlineSchemaSecond.class);
48+
c.resolve(new AnnotatedType(OrderSchema.class));
49+
expectedYaml = "BasicProperty:\n" +
50+
" type: object\n" +
51+
" description: Represents a basic schema property\n" +
52+
"OrderProperty:\n" +
53+
" type: object\n" +
54+
" properties:\n" +
55+
" basicProperty:\n" +
56+
" $ref: '#/components/schemas/BasicProperty'\n" +
57+
" description: Represents an order-specific property\n" +
58+
" example: Order-specific example value\n" +
59+
"OrderSchema:\n" +
60+
" type: object\n" +
61+
" properties:\n" +
62+
" propertyOne:\n" +
63+
" allOf:\n" +
64+
" - type: object\n" +
65+
" description: First order schema property\n" +
66+
" nullable: true\n" +
67+
" - $ref: '#/components/schemas/OrderProperty'\n" +
68+
" userProperty:\n" +
69+
" allOf:\n" +
70+
" - type: object\n" +
71+
" description: \"Order schema property, references UserProperty\"\n" +
72+
" example: example value for userProperty\n" +
73+
" - $ref: '#/components/schemas/UserProperty'\n" +
74+
"UserProperty:\n" +
75+
" type: object\n" +
76+
" description: Represents a user-specific property\n" +
77+
" example: User-specific example value\n" +
78+
"UserSchema:\n" +
79+
" type: object\n" +
80+
" properties:\n" +
81+
" propertyOne:\n" +
82+
" allOf:\n" +
83+
" - type: object\n" +
84+
" description: First user schema property\n" +
85+
" nullable: true\n" +
86+
" - $ref: '#/components/schemas/UserProperty'\n" +
87+
" propertyTwo:\n" +
88+
" allOf:\n" +
89+
" - type: object\n" +
90+
" description: Second user schema property\n" +
91+
" example: example value for propertyTwo\n" +
92+
" - $ref: '#/components/schemas/UserProperty'\n" +
93+
" propertyThree:\n" +
94+
" allOf:\n" +
95+
" - type: object\n" +
96+
" description: \"Third user schema property, with example for testing\"\n" +
97+
" example: example value for propertyThree\n" +
98+
" - $ref: '#/components/schemas/UserProperty'\n";
99+
SerializationMatchers.assertEqualsToYaml(c.getDefinedModels(), expectedYaml);
100+
}
101+
102+
// Renamed class to better describe what it represents
103+
static class UserSchema {
104+
105+
@Schema(description = "First user schema property", nullable = true)
106+
public UserProperty propertyOne;
107+
108+
private UserProperty propertyTwo;
109+
110+
@Schema(description = "Second user schema property", example = "example value for propertyTwo")
111+
public UserProperty getPropertyTwo() {
112+
return propertyTwo;
113+
}
114+
115+
// Third property with no specific annotation. It's good to add some description or example for clarity
116+
@Schema(description = "Third user schema property, with example for testing", example = "example value for propertyThree")
117+
public UserProperty getPropertyThree() {
118+
return null; // returning null as per the test scenario
119+
}
120+
}
121+
122+
// Renamed class to represent a different entity for the schema test
123+
static class OrderSchema {
124+
125+
@Schema(description = "First order schema property", nullable = true)
126+
public OrderProperty propertyOne;
127+
128+
private UserProperty userProperty;
129+
130+
@Schema(description = "Order schema property, references UserProperty", example = "example value for userProperty")
131+
public UserProperty getUserProperty() {
132+
return userProperty;
133+
}
134+
}
135+
136+
// Renamed properties to make them clearer about their role in the schema
137+
@Schema(description = "Represents a user-specific property", example = "User-specific example value")
138+
static class UserProperty {
139+
// public String value;
140+
}
141+
142+
@Schema(description = "Represents an order-specific property", example = "Order-specific example value")
143+
static class OrderProperty {
144+
public BasicProperty basicProperty;
145+
}
146+
147+
static class BasicSchema {
148+
149+
@Schema(description = "First basic schema property")
150+
public BasicProperty propertyOne;
151+
152+
private BasicProperty propertyTwo;
153+
154+
@Schema(description = "Second basic schema property", example = "example value for propertyTwo")
155+
public BasicProperty getPropertyTwo() {
156+
return propertyTwo;
157+
}
158+
}
159+
160+
// Renamed to represent a basic property common in various schemas
161+
@Schema(description = "Represents a basic schema property")
162+
static class BasicProperty {
163+
// public String value;
164+
}
165+
}

0 commit comments

Comments
 (0)