Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

schema resolution options - Phase 2: global allOf #4738

Merged
merged 1 commit into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public ModelConverters(boolean openapi31) {
public ModelConverters(boolean openapi31, Schema.SchemaResolution schemaResolution) {
converters = new CopyOnWriteArrayList<>();
if (openapi31) {
converters.add(new ModelResolver(Json31.mapper()).openapi31(true));
converters.add(new ModelResolver(Json31.mapper()).openapi31(true).schemaResolution(schemaResolution));
} else {
converters.add(new ModelResolver(Json.mapper()).schemaResolution(schemaResolution));
}
Expand Down Expand Up @@ -81,7 +81,7 @@ public static ModelConverters getInstance(boolean openapi31, Schema.SchemaResolu
synchronized (ModelConverters.class) {
if (openapi31) {
if (SINGLETON31 == null) {
SINGLETON31 = new ModelConverters(openapi31);
SINGLETON31 = new ModelConverters(openapi31, Schema.SchemaResolution.DEFAULT);
init(SINGLETON31);
}
return SINGLETON31;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -682,7 +682,7 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context

Annotation[] ctxAnnotation31 = null;

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

AnnotatedType aType = new AnnotatedType()
.type(propType)
.ctxAnnotations(openapi31 ? ctxAnnotation31 : annotations)
.parent(model)
.resolveAsRef(annotatedType.isResolveAsRef())
.jsonViewAnnotation(annotatedType.getJsonViewAnnotation())
.skipSchemaName(true)
.schemaProperty(true)
.components(annotatedType.getComponents())
.propertyName(propName);

if (Schema.SchemaResolution.ALL_OF.equals(this.schemaResolution) || openapi31) {
aType.ctxAnnotations(ctxAnnotation31);
} else {
aType.ctxAnnotations(annotations);
}
final AnnotatedMember propMember = member;
aType.jsonUnwrappedHandler(t -> {
JsonUnwrapped uw = propMember.getAnnotation(JsonUnwrapped.class);
Expand All @@ -726,6 +729,7 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context
});
property = context.resolve(aType);
property = clone(property);
Schema ctxProperty = null;
if (openapi31) {
Optional<Schema> reResolvedProperty = AnnotationsUtils.getSchemaFromAnnotation(ctxSchema, annotatedType.getComponents(), null, openapi31, property, schemaResolution, context);
if (reResolvedProperty.isPresent()) {
Expand All @@ -736,6 +740,16 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context
property = reResolvedProperty.get();
}

} else if (Schema.SchemaResolution.ALL_OF.equals(this.schemaResolution)) {
Optional<Schema> reResolvedProperty = AnnotationsUtils.getSchemaFromAnnotation(ctxSchema, annotatedType.getComponents(), null, openapi31, null, schemaResolution, context);
if (reResolvedProperty.isPresent()) {
ctxProperty = reResolvedProperty.get();
}
reResolvedProperty = AnnotationsUtils.getArraySchema(ctxArraySchema, annotatedType.getComponents(), null, openapi31, ctxProperty);
if (reResolvedProperty.isPresent()) {
ctxProperty = reResolvedProperty.get();
}

}
if (property != null) {
Boolean required = md.getRequired();
Expand Down Expand Up @@ -777,10 +791,15 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context
if (context.getDefinedModels().containsKey(pName)) {
if (Schema.SchemaResolution.INLINE.equals(this.schemaResolution)) {
property = context.getDefinedModels().get(pName);
} else if (Schema.SchemaResolution.ALL_OF.equals(this.schemaResolution) && ctxProperty != null) {
property = new Schema()
.addAllOfItem(ctxProperty)
.addAllOfItem(new Schema().$ref(constructRef(pName)));
} else {
property = new Schema().$ref(constructRef(pName));
}
property = clone(property);
// TODO: why is this needed? is it not handled before?
if (openapi31 || Schema.SchemaResolution.INLINE.equals(this.schemaResolution)) {
Optional<Schema> reResolvedProperty = AnnotationsUtils.getSchemaFromAnnotation(ctxSchema, annotatedType.getComponents(), null, openapi31, property, this.schemaResolution, context);
if (reResolvedProperty.isPresent()) {
Expand All @@ -794,7 +813,13 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context
}
} else if (property.get$ref() != null) {
if (!openapi31) {
property = new Schema().$ref(StringUtils.isNotEmpty(property.get$ref()) ? property.get$ref() : property.getName());
if (Schema.SchemaResolution.ALL_OF.equals(this.schemaResolution) && ctxProperty != null) {
property = new Schema()
.addAllOfItem(ctxProperty)
.addAllOfItem(new Schema().$ref(StringUtils.isNotEmpty(property.get$ref()) ? property.get$ref() : property.getName()));
} else {
property = new Schema().$ref(StringUtils.isNotEmpty(property.get$ref()) ? property.get$ref() : property.getName());
}
} else {
if (StringUtils.isEmpty(property.get$ref())) {
property.$ref(property.getName());
Expand All @@ -807,9 +832,12 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context
if (property != null && io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED.equals(requiredMode)) {
addRequiredItem(model, property.getName());
}
if (ctxProperty == null) {
ctxProperty = property;
}
final boolean applyNotNullAnnotations = io.swagger.v3.oas.annotations.media.Schema.RequiredMode.AUTO.equals(requiredMode);
annotations = addGenericTypeArgumentAnnotationsForOptionalField(propDef, annotations);
applyBeanValidatorAnnotations(propDef, property, annotations, model, applyNotNullAnnotations);
applyBeanValidatorAnnotations(propDef, ctxProperty, annotations, model, applyNotNullAnnotations);

props.add(property);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,12 @@ public static Optional<Schema> getSchemaFromAnnotation(
} else {
schemaObject = new Schema();
}
} else if (Schema.SchemaResolution.ALL_OF.equals(schemaResolution)) {
if (existingSchema == null) {
schemaObject = new Schema();
} else {
schemaObject = existingSchema;
}
}
} else {
if (existingSchema == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,19 @@ public static Parameter applyAnnotations(
String[] methodTypes,
JsonView jsonViewAnnotation,
boolean openapi31) {
return applyAnnotations(parameter, type, annotations, components, classTypes, methodTypes, jsonViewAnnotation, openapi31, null);
}

public static Parameter applyAnnotations(
Parameter parameter,
Type type,
List<Annotation> annotations,
Components components,
String[] classTypes,
String[] methodTypes,
JsonView jsonViewAnnotation,
boolean openapi31,
Schema.SchemaResolution schemaResolution) {

final AnnotationsHelper helper = new AnnotationsHelper(annotations, type);
if (helper.isContext()) {
Expand All @@ -59,17 +72,57 @@ public static Parameter applyAnnotations(
if (paramSchemaOrArrayAnnotation != null) {
reworkedAnnotations.add(paramSchemaOrArrayAnnotation);
}
io.swagger.v3.oas.annotations.media.Schema ctxSchema = AnnotationsUtils.getSchemaAnnotation(annotations.toArray(new Annotation[0]));
io.swagger.v3.oas.annotations.media.ArraySchema ctxArraySchema = AnnotationsUtils.getArraySchemaAnnotation(annotations.toArray(new Annotation[0]));
Annotation[] ctxAnnotation31 = null;

if (Schema.SchemaResolution.ALL_OF.equals(schemaResolution)) {
List<Annotation> ctxAnnotations31List = new ArrayList<>();
if (annotations != null) {
for (Annotation a : annotations) {
if (
!(a instanceof io.swagger.v3.oas.annotations.media.Schema) &&
!(a instanceof io.swagger.v3.oas.annotations.media.ArraySchema)) {
ctxAnnotations31List.add(a);
}
}
ctxAnnotation31 = ctxAnnotations31List.toArray(new Annotation[ctxAnnotations31List.size()]);
}
}
AnnotatedType annotatedType = new AnnotatedType()
.type(type)
.resolveAsRef(true)
.skipOverride(true)
.jsonViewAnnotation(jsonViewAnnotation)
.ctxAnnotations(reworkedAnnotations.toArray(new Annotation[reworkedAnnotations.size()]));
.jsonViewAnnotation(jsonViewAnnotation);

if (Schema.SchemaResolution.ALL_OF.equals(schemaResolution)) {
annotatedType.ctxAnnotations(ctxAnnotation31);
} else {
annotatedType.ctxAnnotations(reworkedAnnotations.toArray(new Annotation[reworkedAnnotations.size()]));
}

final ResolvedSchema resolvedSchema = ModelConverters.getInstance(openapi31).resolveAsResolvedSchema(annotatedType);
final ResolvedSchema resolvedSchema = ModelConverters.getInstance(openapi31, schemaResolution).resolveAsResolvedSchema(annotatedType);

if (resolvedSchema.schema != null) {
parameter.setSchema(resolvedSchema.schema);
Schema resSchema = AnnotationsUtils.clone(resolvedSchema.schema, openapi31);
Schema ctxSchemaObject = null;
if (Schema.SchemaResolution.ALL_OF.equals(schemaResolution)) {
Optional<Schema> reResolvedSchema = AnnotationsUtils.getSchemaFromAnnotation(ctxSchema, annotatedType.getComponents(), null, openapi31, null, schemaResolution, null);
if (reResolvedSchema.isPresent()) {
ctxSchemaObject = reResolvedSchema.get();
}
reResolvedSchema = AnnotationsUtils.getArraySchema(ctxArraySchema, annotatedType.getComponents(), null, openapi31, ctxSchemaObject);
if (reResolvedSchema.isPresent()) {
ctxSchemaObject = reResolvedSchema.get();
}

}
if (Schema.SchemaResolution.ALL_OF.equals(schemaResolution) && ctxSchemaObject != null) {
resSchema = new Schema()
.addAllOfItem(ctxSchemaObject)
.addAllOfItem(resolvedSchema.schema);
}
parameter.setSchema(resSchema);
}
resolvedSchema.referencedSchemas.forEach(components::addSchemas);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package io.swagger.v3.core.resolving;

import io.swagger.v3.core.converter.AnnotatedType;
import io.swagger.v3.core.converter.ModelConverterContextImpl;
import io.swagger.v3.core.jackson.ModelResolver;
import io.swagger.v3.core.matchers.SerializationMatchers;
import io.swagger.v3.oas.annotations.media.Schema;
import org.testng.annotations.Test;

public class AllofResolvingTest extends SwaggerTestBase {

@Test
public void testAllofResolving() {

final ModelResolver modelResolver = new ModelResolver(mapper()).openapi31(false).schemaResolution(io.swagger.v3.oas.models.media.Schema.SchemaResolution.ALL_OF);
final ModelConverterContextImpl c = new ModelConverterContextImpl(modelResolver);
// ModelConverters c = ModelConverters.getInstance(false, io.swagger.v3.oas.models.media.Schema.SchemaResolution.INLINE);
c.resolve(new AnnotatedType(UserSchema.class));

String expectedYaml = "UserProperty:\n" +
" type: object\n" +
" description: Represents a user-specific property\n" +
" example: User-specific example value\n" +
"UserSchema:\n" +
" type: object\n" +
" properties:\n" +
" propertyOne:\n" +
" allOf:\n" +
" - type: object\n" +
" description: First user schema property\n" +
" nullable: true\n" +
" - $ref: '#/components/schemas/UserProperty'\n" +
" propertyTwo:\n" +
" allOf:\n" +
" - type: object\n" +
" description: Second user schema property\n" +
" example: example value for propertyTwo\n" +
" - $ref: '#/components/schemas/UserProperty'\n" +
" propertyThree:\n" +
" allOf:\n" +
" - type: object\n" +
" description: \"Third user schema property, with example for testing\"\n" +
" example: example value for propertyThree\n" +
" - $ref: '#/components/schemas/UserProperty'\n";

SerializationMatchers.assertEqualsToYaml(c.getDefinedModels(), expectedYaml);
// stringSchemaMap = c.readAll(InlineSchemaSecond.class);
c.resolve(new AnnotatedType(OrderSchema.class));
expectedYaml = "BasicProperty:\n" +
" type: object\n" +
" description: Represents a basic schema property\n" +
"OrderProperty:\n" +
" type: object\n" +
" properties:\n" +
" basicProperty:\n" +
" $ref: '#/components/schemas/BasicProperty'\n" +
" description: Represents an order-specific property\n" +
" example: Order-specific example value\n" +
"OrderSchema:\n" +
" type: object\n" +
" properties:\n" +
" propertyOne:\n" +
" allOf:\n" +
" - type: object\n" +
" description: First order schema property\n" +
" nullable: true\n" +
" - $ref: '#/components/schemas/OrderProperty'\n" +
" userProperty:\n" +
" allOf:\n" +
" - type: object\n" +
" description: \"Order schema property, references UserProperty\"\n" +
" example: example value for userProperty\n" +
" - $ref: '#/components/schemas/UserProperty'\n" +
"UserProperty:\n" +
" type: object\n" +
" description: Represents a user-specific property\n" +
" example: User-specific example value\n" +
"UserSchema:\n" +
" type: object\n" +
" properties:\n" +
" propertyOne:\n" +
" allOf:\n" +
" - type: object\n" +
" description: First user schema property\n" +
" nullable: true\n" +
" - $ref: '#/components/schemas/UserProperty'\n" +
" propertyTwo:\n" +
" allOf:\n" +
" - type: object\n" +
" description: Second user schema property\n" +
" example: example value for propertyTwo\n" +
" - $ref: '#/components/schemas/UserProperty'\n" +
" propertyThree:\n" +
" allOf:\n" +
" - type: object\n" +
" description: \"Third user schema property, with example for testing\"\n" +
" example: example value for propertyThree\n" +
" - $ref: '#/components/schemas/UserProperty'\n";
SerializationMatchers.assertEqualsToYaml(c.getDefinedModels(), expectedYaml);
}

// Renamed class to better describe what it represents
static class UserSchema {

@Schema(description = "First user schema property", nullable = true)
public UserProperty propertyOne;

private UserProperty propertyTwo;

@Schema(description = "Second user schema property", example = "example value for propertyTwo")
public UserProperty getPropertyTwo() {
return propertyTwo;
}

// Third property with no specific annotation. It's good to add some description or example for clarity
@Schema(description = "Third user schema property, with example for testing", example = "example value for propertyThree")
public UserProperty getPropertyThree() {
return null; // returning null as per the test scenario
}
}

// Renamed class to represent a different entity for the schema test
static class OrderSchema {

@Schema(description = "First order schema property", nullable = true)
public OrderProperty propertyOne;

private UserProperty userProperty;

@Schema(description = "Order schema property, references UserProperty", example = "example value for userProperty")
public UserProperty getUserProperty() {
return userProperty;
}
}

// Renamed properties to make them clearer about their role in the schema
@Schema(description = "Represents a user-specific property", example = "User-specific example value")
static class UserProperty {
// public String value;
}

@Schema(description = "Represents an order-specific property", example = "Order-specific example value")
static class OrderProperty {
public BasicProperty basicProperty;
}

static class BasicSchema {

@Schema(description = "First basic schema property")
public BasicProperty propertyOne;

private BasicProperty propertyTwo;

@Schema(description = "Second basic schema property", example = "example value for propertyTwo")
public BasicProperty getPropertyTwo() {
return propertyTwo;
}
}

// Renamed to represent a basic property common in various schemas
@Schema(description = "Represents a basic schema property")
static class BasicProperty {
// public String value;
}
}
Loading
Loading