Skip to content
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 @@ -113,6 +113,7 @@ public class ModelResolver extends AbstractModelConverter implements ModelConver

Logger LOGGER = LoggerFactory.getLogger(ModelResolver.class);
public static List<String> NOT_NULL_ANNOTATIONS = Arrays.asList("NotNull", "NonNull", "NotBlank", "NotEmpty");
public static List<String> NULLABLE_ANNOTATIONS = Arrays.asList("Nullable");

public static final String SET_PROPERTY_OF_COMPOSED_MODEL_AS_SIBLING = "composed-model-properties-as-sibiling";
public static final String SET_PROPERTY_OF_ENUMS_AS_REF = "enums-as-ref";
Expand Down Expand Up @@ -2446,6 +2447,13 @@ protected Boolean resolveNullable(Annotated a, Annotation[] annotations, io.swag
if (schema != null && schema.nullable()) {
return schema.nullable();
}
if (annotations != null) {
for (Annotation annotation : annotations) {
if (NULLABLE_ANNOTATIONS.contains(annotation.annotationType().getSimpleName())) {
return true;
}
}
}
return null;
}

Expand Down Expand Up @@ -3143,6 +3151,9 @@ protected void resolveSchemaMembers(Schema schema, Annotated a, Annotation[] ann
Boolean nullable = resolveNullable(a, annotations, schemaAnnotation);
if (nullable != null) {
schema.nullable(nullable);
if (openapi31 && nullable) {
schema.addType("null");
}
}
BigDecimal multipleOf = resolveMultipleOf(a, annotations, schemaAnnotation);
if (multipleOf != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,9 @@ public static Optional<Schema> getSchemaFromAnnotation(
}
if (schema.nullable()) {
schemaObject.setNullable(schema.nullable());
if (openapi31) {
schemaObject.addType("null");
}
}
if (StringUtils.isNotBlank(schema.title())) {
schemaObject.setTitle(schema.title());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package io.swagger.v3.core.issues;

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.util.Configuration;
import io.swagger.v3.core.util.Json;
import io.swagger.v3.core.util.Json31;
import io.swagger.v3.oas.annotations.media.Schema;
import org.testng.annotations.Test;

import javax.annotation.Nullable;
import java.util.Set;

import static org.testng.Assert.*;

/**
* Reproduces GitHub Issue #5001
* Native support for @Nullable annotations to generate proper nullable types
*
* Tests that @Nullable annotation is recognized and generates appropriate nullable output:
* - OAS 3.0: nullable keyword
* - OAS 3.1: type array with "null"
*
* 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.
*
* @see <a href="https://github.com/swagger-api/swagger-core/issues/5001">...</a>
*/
public class Issue5001Test {

/**
* Tests @Nullable annotation with OAS 3.1 (type array output)
*/
@Test
public void testNullableWithOAS31() throws Exception {
final ModelResolver modelResolver = new ModelResolver(Json31.mapper());
Configuration configuration = new Configuration();
configuration.setOpenAPI31(true);
modelResolver.setConfiguration(configuration);
final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver);

final io.swagger.v3.oas.models.media.Schema model = context
.resolve(new AnnotatedType(NullableModel.class));

assertNotNull(model);
assertNotNull(model.getProperties());

// Field with @Nullable should generate type array ["string", "null"]
io.swagger.v3.oas.models.media.Schema nullableField =
(io.swagger.v3.oas.models.media.Schema) model.getProperties().get("nullableString");
assertNotNull(nullableField, "nullableString property should exist");
assertNotNull(nullableField.getTypes(), "@Nullable should generate types array in OAS 3.1");
assertTrue(nullableField.getTypes().contains("string"), "types should include 'string'");
assertTrue(nullableField.getTypes().contains("null"), "types should include 'null'");
assertEquals(((Set<?>) nullableField.getTypes()).size(), 2, "Should have exactly 2 types");

// Non-nullable field should only have string type
io.swagger.v3.oas.models.media.Schema requiredField =
(io.swagger.v3.oas.models.media.Schema) model.getProperties().get("requiredString");
assertNotNull(requiredField);
assertNotNull(requiredField.getTypes());
assertTrue(requiredField.getTypes().contains("string"));
assertFalse(requiredField.getTypes().contains("null"), "Non-nullable field should not have 'null' type");
}

/**
* Tests @Nullable annotation with OAS 3.0 (nullable keyword output)
*/
@Test
public void testNullableWithOAS30() throws Exception {
final ModelResolver modelResolver = new ModelResolver(Json.mapper());
Configuration configuration = new Configuration();
configuration.setOpenAPI31(false);
modelResolver.setConfiguration(configuration);
final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver);

final io.swagger.v3.oas.models.media.Schema model = context
.resolve(new AnnotatedType(NullableModel.class));

assertNotNull(model);
assertNotNull(model.getProperties());

// Field with @Nullable should set nullable=true in OAS 3.0
io.swagger.v3.oas.models.media.Schema nullableField =
(io.swagger.v3.oas.models.media.Schema) model.getProperties().get("nullableString");
assertNotNull(nullableField, "nullableString property should exist");
assertEquals(nullableField.getNullable(), Boolean.TRUE, "@Nullable should set nullable=true in OAS 3.0");
assertEquals(nullableField.getType(), "string", "type should be 'string'");

// Non-nullable field should not have nullable property
io.swagger.v3.oas.models.media.Schema requiredField =
(io.swagger.v3.oas.models.media.Schema) model.getProperties().get("requiredString");
assertNotNull(requiredField);
assertNotEquals(requiredField.getNullable(), Boolean.TRUE, "Non-nullable field should not be nullable");
}

/**
* Tests explicit @Schema annotations with OAS 3.1
*/
@Test
public void testExplicitSchemaAnnotationsWithOAS31() throws Exception {
final ModelResolver modelResolver = new ModelResolver(Json31.mapper());
Configuration configuration = new Configuration();
configuration.setOpenAPI31(true);
modelResolver.setConfiguration(configuration);
final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver);

final io.swagger.v3.oas.models.media.Schema model = context
.resolve(new AnnotatedType(ExplicitSchemaModel.class));

assertNotNull(model);
assertNotNull(model.getProperties());

// @Schema(nullable=true) should set nullable property and generate types array
io.swagger.v3.oas.models.media.Schema explicitNullable =
(io.swagger.v3.oas.models.media.Schema) model.getProperties().get("explicitNullableString");
assertNotNull(explicitNullable);
assertEquals(explicitNullable.getNullable(), Boolean.TRUE, "@Schema(nullable=true) should set nullable");
assertTrue(explicitNullable.getTypes().contains("string"));
assertTrue(explicitNullable.getTypes().contains("null"));

// @Schema(types={"string", "null"}) should work
io.swagger.v3.oas.models.media.Schema explicitTypes =
(io.swagger.v3.oas.models.media.Schema) model.getProperties().get("explicitTypesString");
assertNotNull(explicitTypes);
assertNotNull(explicitTypes.getTypes());
assertTrue(explicitTypes.getTypes().contains("string"));
assertTrue(explicitTypes.getTypes().contains("null"));
}

/**
* Model using @Nullable annotation
* Note: Uses javax.annotation.Nullable which gets transformed to jakarta.annotation.Nullable
* in the swagger-core-jakarta module
*/
public static class NullableModel {
@Nullable
private String nullableString;

private String requiredString;

public String getNullableString() {
return nullableString;
}

public void setNullableString(String nullableString) {
this.nullableString = nullableString;
}

public String getRequiredString() {
return requiredString;
}

public void setRequiredString(String requiredString) {
this.requiredString = requiredString;
}
}

/**
* Model using explicit @Schema annotations
*/
public static class ExplicitSchemaModel {
@Schema(nullable = true)
private String explicitNullableString;

@Schema(types = {"string", "null"})
private String explicitTypesString;

public String getExplicitNullableString() {
return explicitNullableString;
}

public void setExplicitNullableString(String explicitNullableString) {
this.explicitNullableString = explicitNullableString;
}

public String getExplicitTypesString() {
return explicitTypesString;
}

public void setExplicitTypesString(String explicitTypesString) {
this.explicitTypesString = explicitTypesString;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2258,7 +2258,7 @@ public String toString() {
sb.append(" dependentSchemas: ").append(toIndentedString(dependentSchemas)).append("\n");
sb.append(" $comment: ").append(toIndentedString($comment)).append("\n");
sb.append(" prefixItems: ").append(toIndentedString(prefixItems)).append("\n");
sb.append(" booleanSchemaValue").append(toIndentedString(booleanSchemaValue)).append("\n");
sb.append(" booleanSchemaValue: ").append(toIndentedString(booleanSchemaValue)).append("\n");
}
sb.append("}");
return sb.toString();
Expand Down Expand Up @@ -2346,4 +2346,3 @@ public Schema booleanSchemaValue(Boolean booleanSchemaValue) {
return this;
}
}

Loading