Issue5115Test.java
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 java.util.Collections;
import java.util.LinkedHashSet;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertNull;
/**
* 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;
}
}
@Test
public void testComponentSchemaStableRegardlessOfNullableFieldOrderOAS31() {
ResolvedSchema nullableFirst = ModelConverters.getInstance(true)
.readAllAsResolvedSchema(ContainerNullableFirst.class);
ResolvedSchema nonNullableFirst = ModelConverters.getInstance(true)
.readAllAsResolvedSchema(ContainerNonNullableFirst.class);
io.swagger.v3.oas.models.media.Schema nestedFromNullableFirst = nullableFirst.referencedSchemas.get("NestedModel");
io.swagger.v3.oas.models.media.Schema nestedFromNonNullableFirst = nonNullableFirst.referencedSchemas.get("NestedModel");
assertNotNull(nestedFromNullableFirst);
assertNotNull(nestedFromNonNullableFirst);
LinkedHashSet<String> expectedTypes = new LinkedHashSet<>(Collections.singletonList("object"));
assertFalse(nestedFromNullableFirst.getTypes().contains("null"),
"NestedModel must not contain null type when nullable field is resolved first");
assertEquals(nestedFromNullableFirst.getTypes(), expectedTypes);
assertEquals(nestedFromNonNullableFirst.getTypes(), expectedTypes,
"NestedModel component schema must be identical regardless of field resolution order");
}
@Test
public void testComponentSchemaStableRegardlessOfNullableFieldOrderOAS30() {
ResolvedSchema nullableFirst = ModelConverters.getInstance()
.readAllAsResolvedSchema(ContainerNullableFirst.class);
ResolvedSchema nonNullableFirst = ModelConverters.getInstance()
.readAllAsResolvedSchema(ContainerNonNullableFirst.class);
io.swagger.v3.oas.models.media.Schema nestedFromNullableFirst = nullableFirst.referencedSchemas.get("NestedModel");
io.swagger.v3.oas.models.media.Schema nestedFromNonNullableFirst = nonNullableFirst.referencedSchemas.get("NestedModel");
assertNotNull(nestedFromNullableFirst);
assertNotNull(nestedFromNonNullableFirst);
assertEquals(nestedFromNullableFirst.getType(), "object");
assertNull(nestedFromNullableFirst.getNullable(),
"NestedModel must not be nullable when nullable field is resolved first");
assertEquals(nestedFromNonNullableFirst.getType(), "object");
assertNull(nestedFromNonNullableFirst.getNullable(),
"NestedModel component schema must be identical regardless of field resolution order");
}
public static class ContainerNullableFirst {
@Nullable
private NestedModel nullableModel;
private NestedModel model;
@Nullable
public NestedModel getNullableModel() {
return nullableModel;
}
public void setNullableModel(@Nullable NestedModel nullableModel) {
this.nullableModel = nullableModel;
}
public NestedModel getModel() {
return model;
}
public void setModel(NestedModel model) {
this.model = model;
}
}
public static class ContainerNonNullableFirst {
private NestedModel model;
@Nullable
private NestedModel nullableModel;
public NestedModel getModel() {
return model;
}
public void setModel(NestedModel model) {
this.model = model;
}
@Nullable
public NestedModel getNullableModel() {
return nullableModel;
}
public void setNullableModel(@Nullable NestedModel nullableModel) {
this.nullableModel = nullableModel;
}
}
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;
}
}
}