Issue5077Test.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.models.media.ArraySchema;
import io.swagger.v3.oas.models.media.Schema;
import org.testng.annotations.Test;

import javax.annotation.Nullable;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertTrue;

/**
 * Reproduces GitHub Issue #5077
 * Issue using Nullable annotation for OAS 3.1
 *
 * <p>Tests that @Nullable annotation does not affect the item in a list:
 *
 * <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 Issue5077Test {

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

        String expectedJson = ResourceUtils.loadClassResource(getClass(), "specFiles/NullableFieldsOAS31.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 testOAS30() throws IOException {
        ResolvedSchema schema = ModelConverters.getInstance()
                .readAllAsResolvedSchema(ModelWithObjectAndList.class);

        String expectedJson = ResourceUtils.loadClassResource(getClass(), "specFiles/NullableFieldsOAS30.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 nullableSetOas31_onlyContainerIsNullable() {
        ResolvedSchema resolved = ModelConverters.getInstance(true).readAllAsResolvedSchema(NullableSetContainer.class);
        Map<String, Schema> schemas = resolved.referencedSchemas;
        Schema<?> container = schemas.get("NullableSetContainer");
        Schema<?> tags = (Schema<?>) container.getProperties().get("tags");

        assertNotNull(tags);
        assertTrue(tags.getTypes().contains("array"), "container should be array type");
        assertTrue(tags.getTypes().contains("null"), "container should be nullable");
        assertEquals(tags.getTypes().size(), 2);

        Schema<?> items = tags.getItems();
        assertNotNull(items);
        assertTrue(items.getTypes().contains("string"), "items should be string type");
        assertFalse(items.getTypes().contains("null"), "nullable must not leak into items");
    }

    @Test
    public void nullableArrayOas31_onlyContainerIsNullable() {
        ResolvedSchema resolved = ModelConverters.getInstance(true).readAllAsResolvedSchema(NullableArrayContainer.class);
        Map<String, Schema> schemas = resolved.referencedSchemas;
        Schema<?> container = schemas.get("NullableArrayContainer");
        Schema<?> names = (Schema<?>) container.getProperties().get("names");

        assertNotNull(names);
        assertTrue(names.getTypes().contains("array"), "container should be array type");
        assertTrue(names.getTypes().contains("null"), "container should be nullable");
        assertEquals(names.getTypes().size(), 2);

        Schema<?> items = names.getItems();
        assertNotNull(items);
        assertTrue(items.getTypes().contains("string"), "items should be string type");
        assertFalse(items.getTypes().contains("null"), "nullable must not leak into items");
    }

    @Test
    public void nullableMapOas31_onlyContainerIsNullable() {
        ResolvedSchema resolved = ModelConverters.getInstance(true).readAllAsResolvedSchema(NullableMapContainer.class);
        Map<String, Schema> schemas = resolved.referencedSchemas;
        Schema<?> container = schemas.get("NullableMapContainer");
        Schema<?> labels = (Schema<?>) container.getProperties().get("labels");

        assertNotNull(labels);
        assertTrue(labels.getTypes().contains("object"), "container should be object type");
        assertTrue(labels.getTypes().contains("null"), "container should be nullable");
        assertEquals(labels.getTypes().size(), 2);

        Schema<?> additionalProperties = (Schema<?>) labels.getAdditionalProperties();
        assertNotNull(additionalProperties);
        assertTrue(additionalProperties.getTypes().contains("string"), "value schema should be string type");
        assertFalse(additionalProperties.getTypes().contains("null"), "nullable must not leak into value schema");
    }

    @Test
    public void nullableNestedListOas31_onlyOuterContainerIsNullable() {
        ResolvedSchema resolved = ModelConverters.getInstance(true).readAllAsResolvedSchema(NullableNestedListContainer.class);
        Map<String, Schema> schemas = resolved.referencedSchemas;
        Schema<?> container = schemas.get("NullableNestedListContainer");
        Schema<?> matrix = (Schema<?>) container.getProperties().get("matrix");

        assertNotNull(matrix);
        assertTrue(matrix.getTypes().contains("array"), "outer container should be array type");
        assertTrue(matrix.getTypes().contains("null"), "outer container should be nullable");
        assertEquals(matrix.getTypes().size(), 2);

        Schema<?> inner = matrix.getItems();
        assertNotNull(inner);
        assertTrue(inner.getTypes().contains("array"), "inner container should be array type");
        assertFalse(inner.getTypes().contains("null"), "nullable must not leak into inner container");
        assertEquals(inner.getTypes().size(), 1);

        Schema<?> item = inner.getItems();
        assertNotNull(item);
        assertTrue(item.getTypes().contains("string"), "leaf items should be string type");
        assertFalse(item.getTypes().contains("null"), "nullable must not leak into leaf items");
    }

    @Test
    public void explicitItemNullabilityOas31_stillWorks() {
        ResolvedSchema resolved = ModelConverters.getInstance(true).readAllAsResolvedSchema(ExplicitNullableItemsContainer.class);
        Map<String, Schema> schemas = resolved.referencedSchemas;
        Schema<?> container = schemas.get("ExplicitNullableItemsContainer");
        Schema<?> values = (Schema<?>) container.getProperties().get("values");

        assertNotNull(values);
        assertTrue(values.getTypes().contains("array"), "container should be array type");
        assertFalse(values.getTypes().contains("null"), "non-nullable container must not have null type");
        assertEquals(values.getTypes().size(), 1);

        Schema<?> items = values.getItems();
        assertNotNull(items);
        assertTrue(items.getTypes().contains("string"), "items should be string type");
        assertTrue(items.getTypes().contains("null"), "items are explicitly nullable");
        assertEquals(items.getTypes().size(), 2);
    }

    @Test
    public void nullableArraySchemaWithImplementationOas31_onlyContainerIsNullable() {
        ResolvedSchema resolved = ModelConverters.getInstance(true).readAllAsResolvedSchema(NullableArraySchemaWithImplementationContainer.class);
        Map<String, Schema> schemas = resolved.referencedSchemas;
        Schema<?> container = schemas.get("NullableArraySchemaWithImplementationContainer");
        Schema<?> tags = (Schema<?>) container.getProperties().get("tags");

        assertNotNull(tags);
        assertTrue(tags.getTypes().contains("array"), "container should be array type");
        assertTrue(tags.getTypes().contains("null"), "container should be nullable");
        assertEquals(tags.getTypes().size(), 2);

        Schema<?> items = tags.getItems();
        assertNotNull(items);
        assertTrue(items.getTypes().contains("string"), "items should be string type");
        assertFalse(items.getTypes().contains("null"), "nullable must not leak into items");
    }

    @Test
    public void nullableSetOas30_onlyContainerIsNullable() {
        ResolvedSchema resolved = ModelConverters.getInstance(false).readAllAsResolvedSchema(NullableSetContainer.class);
        Map<String, Schema> schemas = resolved.referencedSchemas;
        Schema<?> container = schemas.get("NullableSetContainer");
        ArraySchema tags = (ArraySchema) container.getProperties().get("tags");

        assertNotNull(tags);
        assertEquals(tags.getType(), "array");
        assertTrue(Boolean.TRUE.equals(tags.getNullable()));

        Schema<?> items = tags.getItems();
        assertNotNull(items);
        assertEquals(items.getType(), "string");
        assertNull(items.getNullable(), "nullable must not leak into items");
    }

    @Test
    public void nullableMapOas30_onlyContainerIsNullable() {
        ResolvedSchema resolved = ModelConverters.getInstance(false).readAllAsResolvedSchema(NullableMapContainer.class);
        Map<String, Schema> schemas = resolved.referencedSchemas;
        Schema<?> container = schemas.get("NullableMapContainer");
        Schema<?> labels = (Schema<?>) container.getProperties().get("labels");

        assertNotNull(labels);
        assertEquals(labels.getType(), "object");
        assertTrue(Boolean.TRUE.equals(labels.getNullable()));

        Schema<?> additionalProperties = (Schema<?>) labels.getAdditionalProperties();
        assertNotNull(additionalProperties);
        assertEquals(additionalProperties.getType(), "string");
        assertNull(additionalProperties.getNullable(), "nullable must not leak into value schema");
    }

    public static class ModelWithObjectAndList {

        @Nullable
        private String nullableString;

        private String requiredString;

        @Nullable
        private Model nullableModel;

        @Nullable
        private List<String> strings;

        @Nullable
        private List<Model> models;

        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;
        }

        public Model getNullableModel() {
            return nullableModel;
        }

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

        public List<Model> getModels() {
            return models;
        }

        public void setModels(List<Model> models) {
            this.models = models;
        }

        @Nullable
        public List<String> getStrings() {
            return strings;
        }

        public void setStrings(@Nullable List<String> strings) {
            this.strings = strings;
        }
    }

    public static class NullableSetContainer {
        @Nullable
        public Set<String> tags;
    }

    public static class NullableArrayContainer {
        @Nullable
        public String[] names;
    }

    public static class NullableMapContainer {
        @Nullable
        public Map<String, String> labels;
    }

    public static class NullableNestedListContainer {
        @Nullable
        public List<List<String>> matrix;
    }

    public static class ExplicitNullableItemsContainer {
        @io.swagger.v3.oas.annotations.media.ArraySchema(schema = @io.swagger.v3.oas.annotations.media.Schema(types = { "string", "null" }))
        public List<String> values;
    }

    public static class NullableStreamContainer {
        @Nullable
        @io.swagger.v3.oas.annotations.media.ArraySchema(schema = @io.swagger.v3.oas.annotations.media.Schema(implementation = String.class))
        public Stream<String> tags;
    }

    public static class NullableArraySchemaWithImplementationContainer {
        @Nullable
        @io.swagger.v3.oas.annotations.media.ArraySchema(schema = @io.swagger.v3.oas.annotations.media.Schema(implementation = String.class))
        public List<Object> tags;
    }

    public static class NullableArraySchemaWithRefContainer {
        @Nullable
        @io.swagger.v3.oas.annotations.media.ArraySchema(schema = @io.swagger.v3.oas.annotations.media.Schema(ref = "#/components/schemas/Model"))
        public Object models;
    }

    public static class NullableRefStreamContainer {
        @Nullable
        @io.swagger.v3.oas.annotations.media.ArraySchema(schema = @io.swagger.v3.oas.annotations.media.Schema(implementation = Model.class))
        public Stream<Model> models;
    }

    public static class Model {
        private String field;

        public String getField() {
            return field;
        }

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

}