Skip to content
Open
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 @@ -124,6 +124,7 @@ public class ModelResolver extends AbstractModelConverter implements ModelConver
public static boolean composedModelPropertiesAsSibling = System.getProperty(SET_PROPERTY_OF_COMPOSED_MODEL_AS_SIBLING) != null;

private static final int SCHEMA_COMPONENT_PREFIX = "#/components/schemas/".length();
private static final String OBJECT_TYPE = "object";

private static final Predicate<Annotation> ANNOTATIONS_THAT_SHOULD_BE_STRIPPED_FOR_CONTAINER_ITEMS = annotation ->
annotation.annotationType().getName().startsWith("io.swagger") ||
Expand Down Expand Up @@ -179,19 +180,9 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context
}

final Annotation resolvedSchemaOrArrayAnnotation = AnnotationsUtils.mergeSchemaAnnotations(annotatedType.getCtxAnnotations(), type);
final io.swagger.v3.oas.annotations.media.Schema resolvedSchemaAnnotation =
resolvedSchemaOrArrayAnnotation == null ?
null :
resolvedSchemaOrArrayAnnotation instanceof io.swagger.v3.oas.annotations.media.ArraySchema ?
((io.swagger.v3.oas.annotations.media.ArraySchema) resolvedSchemaOrArrayAnnotation).schema() :
(io.swagger.v3.oas.annotations.media.Schema) resolvedSchemaOrArrayAnnotation;

final io.swagger.v3.oas.annotations.media.ArraySchema resolvedArrayAnnotation =
resolvedSchemaOrArrayAnnotation == null ?
null :
resolvedSchemaOrArrayAnnotation instanceof io.swagger.v3.oas.annotations.media.ArraySchema ?
(io.swagger.v3.oas.annotations.media.ArraySchema) resolvedSchemaOrArrayAnnotation :
null;
final io.swagger.v3.oas.annotations.media.Schema resolvedSchemaAnnotation = getSchemaAnnotation(resolvedSchemaOrArrayAnnotation);

final io.swagger.v3.oas.annotations.media.ArraySchema resolvedArrayAnnotation = getArraySchemaAnnotation(resolvedSchemaOrArrayAnnotation);

final BeanDescription beanDesc;
{
Expand Down Expand Up @@ -744,12 +735,7 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context
propName = propSchemaName;
}
Annotation propSchemaOrArray = AnnotationsUtils.mergeSchemaAnnotations(annotations, propType);
final io.swagger.v3.oas.annotations.media.Schema propResolvedSchemaAnnotation =
propSchemaOrArray == null ?
null :
propSchemaOrArray instanceof io.swagger.v3.oas.annotations.media.ArraySchema ?
((io.swagger.v3.oas.annotations.media.ArraySchema) propSchemaOrArray).arraySchema() :
(io.swagger.v3.oas.annotations.media.Schema) propSchemaOrArray;
final io.swagger.v3.oas.annotations.media.Schema propResolvedSchemaAnnotation = getSchemaAnnotationForArray(propSchemaOrArray);

io.swagger.v3.oas.annotations.media.Schema.AccessMode accessMode = resolveAccessMode(propDef, type, propResolvedSchemaAnnotation);
io.swagger.v3.oas.annotations.media.Schema.RequiredMode requiredMode = resolveRequiredMode(propResolvedSchemaAnnotation, propType);
Expand Down Expand Up @@ -1123,11 +1109,7 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context
}
// check if it has "object" related keywords
if (isInferredObjectSchema(model) && model.get$ref() == null) {
if (openapi31 && model.getTypes() == null) {
model.addType("object");
} else if (!openapi31 && model.getType() == null) {
model.type("object");
}
setSchemaTypeForObjectSchema(model, resolvedSchemaAnnotation);
}
Schema.SchemaResolution resolvedSchemaResolution = AnnotationsUtils.resolveSchemaResolution(this.schemaResolution, resolvedSchemaAnnotation);

Expand Down Expand Up @@ -1165,6 +1147,24 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context
return model;
}

private void setSchemaTypeForObjectSchema(Schema model, io.swagger.v3.oas.annotations.media.Schema schemaAnnotation) {
if (openapi31) {
if (model.getTypes() == null) {
model.addType(OBJECT_TYPE);
}
if (!isNullableSchema(model, schemaAnnotation)) {
model.setTypes(new LinkedHashSet<>(Collections.singletonList(OBJECT_TYPE)));
}
} else {
if (model.getType() == null) {
model.type(OBJECT_TYPE);
}
if (!isNullableSchema(model, schemaAnnotation)) {
model.setNullable(null);
}
}
}

private Annotation[] addGenericTypeArgumentAnnotationsForOptionalField(BeanPropertyDefinition propDef, Annotation[] annotations) {

boolean isNotOptionalType = Optional.ofNullable(propDef)
Expand Down Expand Up @@ -3040,12 +3040,7 @@ protected void resolveSchemaMembers(Schema schema, AnnotatedType annotatedType,
}

final Annotation resolvedSchemaOrArrayAnnotation = AnnotationsUtils.mergeSchemaAnnotations(annotatedType.getCtxAnnotations(), type);
final io.swagger.v3.oas.annotations.media.Schema schemaAnnotation =
resolvedSchemaOrArrayAnnotation == null ?
null :
resolvedSchemaOrArrayAnnotation instanceof io.swagger.v3.oas.annotations.media.ArraySchema ?
((io.swagger.v3.oas.annotations.media.ArraySchema) resolvedSchemaOrArrayAnnotation).schema() :
(io.swagger.v3.oas.annotations.media.Schema) resolvedSchemaOrArrayAnnotation;
final io.swagger.v3.oas.annotations.media.Schema schemaAnnotation = getSchemaAnnotation(resolvedSchemaOrArrayAnnotation);

final BeanDescription beanDesc = _mapper.getSerializationConfig().introspect(type);
Annotated a = beanDesc.getClassInfo();
Expand Down Expand Up @@ -3489,6 +3484,25 @@ public void setConfiguration(Configuration configuration) {
}
}

/**
* Currently {@code null} is not a valid type for any {@code object} other than {@code AdditionalProperties}.
* This since the resolver currently does not produce proper nullable ref:s
* @param schema The schema that should be classified
* @param schemaAnnotation The schema annotation
* @return Whether the schema is considered valid for having the {@code null} type
*/
private boolean isNullableSchema(Schema schema, io.swagger.v3.oas.annotations.media.Schema schemaAnnotation) {
if (openapi31) {
return isObjectSchema(schema) && schema.getAdditionalProperties() != null;
} else {
// If the schema annotation has explicitly set nullable to true, then keep that setting
if (schemaAnnotation != null && schemaAnnotation.nullable()) {
return true;
}
return isObjectSchema(schema) && schema.getAdditionalProperties() != null;
}
}

protected boolean isObjectSchema(Schema schema) {
return SchemaTypeUtils.isObjectSchema(schema);
}
Expand Down Expand Up @@ -3564,6 +3578,36 @@ private Optional<Schema> resolveArraySchemaWithCycleGuard(
return reResolvedProperty;
}

private io.swagger.v3.oas.annotations.media.Schema getSchemaAnnotationForArray(Annotation annotation) {
if (annotation == null) {
return null;
} else {
return annotation instanceof io.swagger.v3.oas.annotations.media.ArraySchema ?
((io.swagger.v3.oas.annotations.media.ArraySchema) annotation).arraySchema() :
(io.swagger.v3.oas.annotations.media.Schema) annotation;
}
}

private io.swagger.v3.oas.annotations.media.Schema getSchemaAnnotation(Annotation annotation) {
if (annotation == null) {
return null;
} else {
return annotation instanceof io.swagger.v3.oas.annotations.media.ArraySchema ?
((io.swagger.v3.oas.annotations.media.ArraySchema) annotation).schema() :
(io.swagger.v3.oas.annotations.media.Schema) annotation;
}
}

private io.swagger.v3.oas.annotations.media.ArraySchema getArraySchemaAnnotation(Annotation annotation) {
if (annotation == null) {
return null;
} else {
return annotation instanceof io.swagger.v3.oas.annotations.media.ArraySchema ?
(io.swagger.v3.oas.annotations.media.ArraySchema) annotation :
null;
}
}

/**
* Checks if the given JavaType represents a java.util.stream.Stream
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
package io.swagger.v3.core.issues;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.core.converter.ModelConverters;
import io.swagger.v3.core.converter.ResolvedSchema;
import io.swagger.v3.core.util.Json;
import io.swagger.v3.core.util.Json31;
import io.swagger.v3.core.util.ResourceUtils;
import io.swagger.v3.oas.annotations.media.Schema;
import org.testng.annotations.Test;

import javax.annotation.Nullable;
import java.io.IOException;

import static org.testng.Assert.assertEquals;

/**
* Reproduces GitHub Issue #5115
* Issue using Nullable annotation on Model-fields
*
* <p>Tests that @Nullable annotation does not affect an object in an invalid manner. Any nullability that is configured
* with a schema annotation (nullable = true for OAS30 and null in types for OAS31) is retained.
*
* <p>Note: This test uses javax.annotation.Nullable which is automatically transformed to
* jakarta.annotation.Nullable in the swagger-core-jakarta module via the Eclipse Transformer.
*/
public class Issue5115Test {

@Test
public void testObjectDoesNotGetInvalidNullableSchemaOAS31() throws IOException {
ResolvedSchema schema = ModelConverters.getInstance(true)
.readAllAsResolvedSchema(ModelWithObject.class);

String expectedJson = ResourceUtils.loadClassResource(getClass(), "specFiles/NullableObjectFieldsOAS31.json");
String actualJson = Json31.pretty(schema);
ObjectMapper mapper = new ObjectMapper();
JsonNode expectedNode = mapper.readTree(expectedJson);
JsonNode actualNode = mapper.readTree(actualJson);
assertEquals(actualNode, expectedNode);
}

@Test
public void testObjectDoesNotGetInvalidNullableSchemaOAS30() throws IOException {
ResolvedSchema schema = ModelConverters.getInstance()
.readAllAsResolvedSchema(ModelWithObject.class);

String expectedJson = ResourceUtils.loadClassResource(getClass(), "specFiles/NullableObjectFieldsOAS30.json");
String actualJson = Json.pretty(schema);
ObjectMapper mapper = new ObjectMapper();
JsonNode expectedNode = mapper.readTree(expectedJson);
JsonNode actualNode = mapper.readTree(actualJson);
assertEquals(actualNode, expectedNode);
}

@Test
public void testObjectKeepsInvalidNullableSchemaIfSetInSchemaAnnotationOAS31() throws IOException {
ResolvedSchema schema = ModelConverters.getInstance(true)
.readAllAsResolvedSchema(ModelWithObjectThatHasNullableInSchemaAnnotationOAS31.class);

String expectedJson = ResourceUtils.loadClassResource(getClass(), "specFiles/NullableObjectFieldsSchemaAnnotationOAS31.json");
String actualJson = Json31.pretty(schema);
ObjectMapper mapper = new ObjectMapper();
JsonNode expectedNode = mapper.readTree(expectedJson);
JsonNode actualNode = mapper.readTree(actualJson);
assertEquals(actualNode, expectedNode);
}

@Test
public void testObjectKeepsInvalidNullableSchemaIfSetInSchemaAnnotationOAS30() throws IOException {
ResolvedSchema schema = ModelConverters.getInstance()
.readAllAsResolvedSchema(ModelWithObjectThatHasNullableInSchemaAnnotationOAS30.class);

String expectedJson = ResourceUtils.loadClassResource(getClass(), "specFiles/NullableObjectFieldsSchemaAnnotationOAS30.json");
String actualJson = Json.pretty(schema);
ObjectMapper mapper = new ObjectMapper();
JsonNode expectedNode = mapper.readTree(expectedJson);
JsonNode actualNode = mapper.readTree(actualJson);
assertEquals(actualNode, expectedNode);
}

public static class ModelWithObject {

@Nullable
private Model nullableModel;

private Model model;

@Nullable
public Model getNullableModel() {
return nullableModel;
}

public void setNullableModel(@Nullable Model model) {
this.nullableModel = model;
}

public Model getModel() {
return model;
}

public void setModel(Model model) {
this.model = model;
}
}

public static class ModelWithObjectThatHasNullableInSchemaAnnotationOAS30 {

// This nullable is lost since the fields are read top to bottom and model does not define nullable
@Schema(nullable = true)
private NestedModel nullableModel;

private NestedModel model;

@Schema(nullable = true)
private NestedModel2 nullableModel2;

public NestedModel getNullableModel() {
return nullableModel;
}

public void setNullableModel(NestedModel model) {
this.nullableModel = model;
}

public NestedModel getModel() {
return model;
}

public void setModel(NestedModel model) {
this.model = model;
}

public NestedModel2 getNullableModel2() {
return nullableModel2;
}

public void setNullableModel2(NestedModel2 nullableModel2) {
this.nullableModel2 = nullableModel2;
}
}

public static class ModelWithObjectThatHasNullableInSchemaAnnotationOAS31 {

@Schema(types = {"object", "null"})
private Model nullableModel;

private Model model;

public Model getNullableModel() {
return nullableModel;
}

public void setNullableModel(Model model) {
this.nullableModel = model;
}

public Model getModel() {
return model;
}

public void setModel(Model model) {
this.model = model;
}
}

public static class Model {

private NestedModel nestedModel;

@Nullable
private NestedModel nullableNestedModel;

@Nullable
private NestedModel2 nullableNestedModel2;

private NestedModel2 nestedModel2;

public NestedModel getNestedModel() {
return nestedModel;
}

public void setNestedModel(NestedModel nestedModel) {
this.nestedModel = nestedModel;
}

@Nullable
public NestedModel getNullableNestedModel() {
return nullableNestedModel;
}

public void setNullableNestedModel(@Nullable NestedModel nullableNestedModel) {
this.nullableNestedModel = nullableNestedModel;
}

@Nullable
public NestedModel2 getNullableNestedModel2() {
return nullableNestedModel2;
}

public void setNullableNestedModel2(@Nullable NestedModel2 nullableNestedModel2) {
this.nullableNestedModel2 = nullableNestedModel2;
}

public NestedModel2 getNestedModel2() {
return nestedModel2;
}

public void setNestedModel2(NestedModel2 nestedModel2) {
this.nestedModel2 = nestedModel2;
}
}

public static class NestedModel {
private String field;

public String getField() {
return field;
}

public void setField(String field) {
this.field = field;
}
}

public static class NestedModel2 {
private String field2;

public String getField2() {
return field2;
}

public void setField2(String field2) {
this.field2 = field2;
}
}

}
Loading