diff --git a/modules/swagger-core/src/main/java/io/swagger/v3/core/jackson/ModelResolver.java b/modules/swagger-core/src/main/java/io/swagger/v3/core/jackson/ModelResolver.java index 64e2b036e0..8310bed91a 100644 --- a/modules/swagger-core/src/main/java/io/swagger/v3/core/jackson/ModelResolver.java +++ b/modules/swagger-core/src/main/java/io/swagger/v3/core/jackson/ModelResolver.java @@ -39,6 +39,7 @@ import io.swagger.v3.core.util.ReferenceTypeUtils; import io.swagger.v3.core.util.PrimitiveType; import io.swagger.v3.core.util.ReflectionUtils; +import io.swagger.v3.core.util.AnnotationsIntrospector; import io.swagger.v3.core.util.ValidatorProcessor; import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; @@ -347,7 +348,7 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context model = openapi31 ? primitiveType.createProperty31() : primitiveType.createProperty(); isPrimitive = true; } - } + } if (model == null) { PrimitiveType primitiveType = PrimitiveType.fromType(type); @@ -699,12 +700,10 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context if (member != null && !ignore(member, xmlAccessorTypeAnnotation, propName, propertiesToIgnore, propDef)) { - List annotationList = new ArrayList<>(); - for (Annotation a : member.annotations()) { - annotationList.add(a); - } + List memberAnnotations = new ArrayList<>(); + resolveAnnotateMemberAnnotations(member, memberAnnotations, true); - annotations = annotationList.toArray(new Annotation[annotationList.size()]); + annotations = memberAnnotations.toArray(new Annotation[memberAnnotations.size()]); if (hiddenByJsonView(annotations, annotatedType)) { continue; @@ -1173,6 +1172,10 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context return model; } + protected void resolveAnnotateMemberAnnotations(AnnotatedMember member, List output, boolean includeDefault) { + AnnotationsIntrospector.getAnnotations(member, output, includeDefault); + } + private Annotation[] addGenericTypeArgumentAnnotationsForOptionalField(BeanPropertyDefinition propDef, Annotation[] annotations) { boolean isNotOptionalType = Optional.ofNullable(propDef) @@ -1838,6 +1841,21 @@ protected boolean applyBeanValidatorAnnotations(Schema property, Annotation[] an } } + // jspecify + if (parent != null && applyNotNullAnnotations && acceptNoGroups) { + boolean nullable = annos.containsKey("org.jspecify.annotations.Nullable"); + boolean nonnull = annos.containsKey("org.jspecify.annotations.NonNull"); + boolean nullmarked = annos.containsKey("org.jspecify.annotations.NullMarked") && !annos.containsKey("org.jspecify.annotations.NullUnmarked"); + if ((nullmarked && !nullable) || nonnull) { + modified = updateRequiredItem(parent, property.getName()) || modified; + } + if (nullable) { + property.setNullable(true); + property.addType("null"); + modified = true; + } + } + if (annos.containsKey("javax.validation.constraints.NotEmpty")) { NotEmpty anno = (NotEmpty) annos.get("javax.validation.constraints.NotEmpty"); boolean apply = checkGroupValidation(anno.groups(), invocationGroups, acceptNoGroups); @@ -1996,6 +2014,22 @@ protected boolean applyBeanValidatorAnnotationsNoGroups(Schema property, Annotat modified = updateRequiredItem(parent, property.getName()); } } + + // jspecify + if (parent != null && annotations != null && applyNotNullAnnotations) { + boolean nullable = annos.containsKey("org.jspecify.annotations.Nullable"); + boolean nonnull = annos.containsKey("org.jspecify.annotations.NonNull"); + boolean nullmarked = annos.containsKey("org.jspecify.annotations.NullMarked") && !annos.containsKey("org.jspecify.annotations.NullUnmarked"); + if ((nullmarked && !nullable) || nonnull) { + modified = updateRequiredItem(parent, property.getName()) || modified; + } + if (nullable) { + property.setNullable(true); + property.addType("null"); + modified = true; + } + } + if (annos.containsKey("javax.validation.constraints.Min")) { if (isNumberSchema(property)) { Min min = (Min) annos.get("javax.validation.constraints.Min"); diff --git a/modules/swagger-core/src/main/java/io/swagger/v3/core/util/AnnotationsIntrospector.java b/modules/swagger-core/src/main/java/io/swagger/v3/core/util/AnnotationsIntrospector.java new file mode 100644 index 0000000000..7955ef35ff --- /dev/null +++ b/modules/swagger-core/src/main/java/io/swagger/v3/core/util/AnnotationsIntrospector.java @@ -0,0 +1,287 @@ +package io.swagger.v3.core.util; + +import com.fasterxml.jackson.databind.introspect.AnnotatedField; +import com.fasterxml.jackson.databind.introspect.AnnotatedMember; +import com.fasterxml.jackson.databind.introspect.AnnotatedMethod; +import com.fasterxml.jackson.databind.introspect.AnnotatedParameter; +import com.fasterxml.jackson.databind.introspect.AnnotatedWithParams; + +import java.lang.annotation.Annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; +import java.lang.reflect.AnnotatedArrayType; +import java.lang.reflect.AnnotatedParameterizedType; +import java.lang.reflect.AnnotatedType; +import java.lang.reflect.AnnotatedTypeVariable; +import java.lang.reflect.AnnotatedWildcardType; +import java.lang.reflect.Executable; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.List; + +public final class AnnotationsIntrospector { + + public static AnnotatedType toAnnotatedType(AnnotatedMember member) { + if (member instanceof AnnotatedField) { + AnnotatedField af = (AnnotatedField) member; + // Underlying java.lang.reflect.Field + return af.getAnnotated().getAnnotatedType(); + } + + if (member instanceof AnnotatedMethod) { + AnnotatedMethod am = (AnnotatedMethod) member; + final Method m = am.getAnnotated(); + final int pc = am.getParameterCount(); + if (pc == 0) { + // Likely a getter + return m.getAnnotatedReturnType(); + } else if (pc == 1) { + // Likely a setter + return m.getParameters()[0].getAnnotatedType(); + } else { + // Unusual shape (e.g., @JsonCreator method with multiple params) + // Choose the user’s param via other context if you have it; here we fail fast. + throw new IllegalArgumentException("Method has " + pc + " parameters; cannot choose which parameter's AnnotatedType to return."); + } + } + + if (member instanceof AnnotatedParameter) { + AnnotatedParameter ap = (AnnotatedParameter) member; + // For parameters, do not use ap.getAnnotated() (often null); go via the owner Executable. + final AnnotatedWithParams owner = ap.getOwner(); + final Executable exec = (Executable) owner.getAnnotated(); + final int index = ap.getIndex(); + final Parameter[] params = exec.getParameters(); + if (index < 0 || index >= params.length) { + throw new IllegalArgumentException("Parameter index out of bounds: " + index + " for " + exec); + } + return params[index].getAnnotatedType(); + } + + // (Very rare) Other AnnotatedMember subtypes or custom implementations + throw new IllegalArgumentException("Unsupported AnnotatedMember subtype: " + member.getClass().getName()); + } + + public static void getAnnotations(AnnotatedMember t, List out, boolean includeDefault) { + if (t == null) { + return; + } + if (includeDefault) { + for (Annotation a : t.annotations()) { + out.add(a); + } + } + + // Collect annotations from the declaring class of the member + Class declaringClass = t.getDeclaringClass(); + if (declaringClass != null) { + collectClassAnnotations(declaringClass, out, true); + collectPackageAnnotations(declaringClass, out, true); + } + + getAnnotations(toAnnotatedType(t), out, true); + } + + public static void getAnnotations(AnnotatedType t, List out, boolean onlyNullability) { + if (t == null) { + return; + } + // Collect TYPE_USE annotations from the current type level + for (Annotation ann : t.getAnnotations()) { + // Check if this annotation can be applied to TYPE_USE + Target target = ann.annotationType().getAnnotation(Target.class); + if (target != null) { + for (ElementType elementType : target.value()) { + if (elementType == ElementType.TYPE_USE) { + String annTypeName = ann.annotationType().getName(); + if ((annTypeName.equals("org.jspecify.annotations.NullMarked") || + annTypeName.equals("org.jspecify.annotations.NullUnmarked") || + annTypeName.equals("org.jspecify.annotations.Nullable") || + annTypeName.equals("org.jspecify.annotations.NonNull")) && onlyNullability) { + out.add(ann); + break; + } else if (!onlyNullability) { + out.add(ann); + break; + } + } + } + } + } + + AnnotatedType owner = ownerOf(t); + if (owner != null) { + getAnnotations(owner, out, onlyNullability); + } + + if (t instanceof AnnotatedParameterizedType) { + for (AnnotatedType arg : ((AnnotatedParameterizedType) t).getAnnotatedActualTypeArguments()) { + getAnnotations(arg, out, onlyNullability); + } + } else if (t instanceof AnnotatedArrayType) { + getAnnotations(((AnnotatedArrayType) t).getAnnotatedGenericComponentType(), out, onlyNullability); + } else if (t instanceof AnnotatedWildcardType) { + for (AnnotatedType lb : ((AnnotatedWildcardType) t).getAnnotatedLowerBounds()) { + getAnnotations(lb, out, onlyNullability); + } + for (AnnotatedType ub : ((AnnotatedWildcardType) t).getAnnotatedUpperBounds()) { + getAnnotations(ub, out, onlyNullability); + } + } else if (t instanceof AnnotatedTypeVariable) { + for (AnnotatedType b : ((AnnotatedTypeVariable) t).getAnnotatedBounds()) { + getAnnotations(b, out, onlyNullability); + } + } + } + + private static void collectPackageAnnotations(Class clazz, List out, boolean onlyNullability) { + Package pkg = clazz.getPackage(); + String packageName = pkg != null ? pkg.getName() : null; + + while (packageName != null && !packageName.isEmpty()) { + try { + Class packageInfo = Class.forName(packageName + ".package-info"); + Package packageFromInfo = packageInfo.getPackage(); + + for (Annotation ann : packageFromInfo.getAnnotations()) { + // Check if this annotation can be applied to TYPE_USE, PACKAGE, or is specifically @NullMarked + Target target = ann.annotationType().getAnnotation(Target.class); + boolean shouldInclude = false; + + if (target != null) { + for (ElementType elementType : target.value()) { + if (!onlyNullability && (elementType == ElementType.TYPE_USE || elementType == ElementType.PACKAGE)) { + shouldInclude = true; + break; + } + } + } + + // Special handling for @NullMarked and other nullability annotations + String annTypeName = ann.annotationType().getName(); + if (annTypeName.equals("org.jspecify.annotations.NullMarked") || + annTypeName.equals("org.jspecify.annotations.NullUnmarked") || + annTypeName.equals("org.jspecify.annotations.Nullable") || + annTypeName.equals("org.jspecify.annotations.NonNull")) { + shouldInclude = true; + } + + if (shouldInclude) { + out.add(ann); + } + } + } catch (ClassNotFoundException e) { + // No package-info.java found for this package level + } + + // Move to parent package + int lastDot = packageName.lastIndexOf('.'); + if (lastDot > 0) { + packageName = packageName.substring(0, lastDot); + } else { + break; + } + } + } + + + private static void collectClassAnnotations(Class clazz, List out, boolean onlyNullability) { + // Traverse up the class hierarchy to collect annotations + Class currentClass = clazz; + while (currentClass != null && currentClass != Object.class) { + for (Annotation ann : currentClass.getAnnotations()) { + // Check if this annotation can be applied to TYPE, TYPE_USE, or is specifically a nullability annotation + Target target = ann.annotationType().getAnnotation(Target.class); + boolean shouldInclude = false; + + if (target != null) { + for (ElementType elementType : target.value()) { + if (!onlyNullability && (elementType == ElementType.TYPE || elementType == ElementType.TYPE_USE)) { + shouldInclude = true; + break; + } + } + } + + // Special handling for @NullMarked and other nullability annotations + String annTypeName = ann.annotationType().getName(); + if (annTypeName.equals("org.jspecify.annotations.NullMarked") || + annTypeName.equals("org.jspecify.annotations.NullUnmarked") || + annTypeName.equals("org.jspecify.annotations.Nullable") || + annTypeName.equals("org.jspecify.annotations.NonNull")) { + shouldInclude = true; + } + + if (shouldInclude) { + out.add(ann); + } + } + + // Move to superclass + currentClass = currentClass.getSuperclass(); + } + } + + private static void walk(AnnotatedType t, Class annoType, List out) { + if (t == null) { + return; + } + + A hit = t.getAnnotation(annoType); + if (hit != null) { + out.add(hit); + } + + // Cross-JDK owner handling + AnnotatedType owner = ownerOf(t); + if (owner != null) { + walk(owner, annoType, out); + } + + if (t instanceof AnnotatedParameterizedType) { + for (AnnotatedType arg : ((AnnotatedParameterizedType) t).getAnnotatedActualTypeArguments()) { + walk(arg, annoType, out); + } + } else if (t instanceof AnnotatedArrayType) { + walk(((AnnotatedArrayType) t).getAnnotatedGenericComponentType(), annoType, out); + } else if (t instanceof AnnotatedWildcardType) { + for (AnnotatedType lb : ((AnnotatedWildcardType) t).getAnnotatedLowerBounds()) { + walk(lb, annoType, out); + } + for (AnnotatedType ub : ((AnnotatedWildcardType) t).getAnnotatedUpperBounds()) { + walk(ub, annoType, out); + } + } else if (t instanceof AnnotatedTypeVariable) { + for (AnnotatedType b : ((AnnotatedTypeVariable) t).getAnnotatedBounds()) { + walk(b, annoType, out); + } + } + } + + /** + * Works on Java 8 (no method) and newer (method exists). + */ + private static AnnotatedType ownerOf(AnnotatedType t) { + // Java 8: only AnnotatedParameterizedType exposes an owner, use that first + if (t instanceof AnnotatedParameterizedType) { + try { + Method m = AnnotatedParameterizedType.class.getMethod("getAnnotatedOwnerType"); + return (AnnotatedType) m.invoke(t); + } catch (NoSuchMethodException e) { + return null; // Running/compiling against Java 8 + } catch (IllegalAccessException | InvocationTargetException e) { + return null; + } + } + // Java 9+: AnnotatedType has default getAnnotatedOwnerType(); call reflectively + try { + Method m = AnnotatedType.class.getMethod("getAnnotatedOwnerType"); + return (AnnotatedType) m.invoke(t); + } catch (NoSuchMethodException e) { + return null; // Running/compiling against Java 8 + } catch (IllegalAccessException | InvocationTargetException e) { + return null; + } + } +} diff --git a/modules/swagger-jaxrs2/pom.xml b/modules/swagger-jaxrs2/pom.xml index 04f55a8f69..3767a5401e 100644 --- a/modules/swagger-jaxrs2/pom.xml +++ b/modules/swagger-jaxrs2/pom.xml @@ -297,5 +297,11 @@ jackson-jaxrs-json-provider ${jackson-version} + + org.jspecify + jspecify + 1.0.0 + test + diff --git a/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/ReaderTest.java b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/ReaderTest.java index 7d2eea8c2b..c4aab9f8d5 100644 --- a/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/ReaderTest.java +++ b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/ReaderTest.java @@ -90,6 +90,7 @@ import io.swagger.v3.jaxrs2.resources.Ticket4804NotBlankResource; import io.swagger.v3.jaxrs2.resources.Ticket4804ProcessorResource; import io.swagger.v3.jaxrs2.resources.Ticket4804Resource; +import io.swagger.v3.jaxrs2.resources.Ticket4848ProcessorResource; import io.swagger.v3.jaxrs2.resources.Ticket4850Resource; import io.swagger.v3.jaxrs2.resources.Ticket4859Resource; import io.swagger.v3.jaxrs2.resources.Ticket4878Resource; @@ -5503,4 +5504,198 @@ public void testTicket4907() { SerializationMatchers.assertEqualsToYaml31(openAPI, yaml); ModelConverters.reset(); } + + @Test(description = "Correctly process JSpecify annotations") + public void testTicket4848Processor() { + ModelConverters.reset(); + SwaggerConfiguration config = new SwaggerConfiguration().schemaResolution(Schema.SchemaResolution.INLINE); + Reader reader = new Reader(config); + + OpenAPI openAPI = reader.read(Ticket4848ProcessorResource.class); + String yaml = "openapi: 3.0.1\n" + + "paths:\n" + + " /test/createpet:\n" + + " post:\n" + + " operationId: postPet\n" + + " requestBody:\n" + + " content:\n" + + " '*/*':\n" + + " schema:\n" + + " required:\n" + + " - address2\n" + + " - street\n" + + " type: object\n" + + " properties:\n" + + " street:\n" + + " type: string\n" + + " city:\n" + + " type: string\n" + + " nullable: true\n" + + " address2:\n" + + " type: string\n" + + " responses:\n" + + " default:\n" + + " description: default response\n" + + " content:\n" + + " '*/*': {}\n" + + " /test/updateperson:\n" + + " put:\n" + + " operationId: putPerson\n" + + " requestBody:\n" + + " content:\n" + + " '*/*':\n" + + " schema:\n" + + " required:\n" + + " - address\n" + + " - department\n" + + " - firstName\n" + + " - id\n" + + " type: object\n" + + " properties:\n" + + " id:\n" + + " type: integer\n" + + " format: int64\n" + + " firstName:\n" + + " type: string\n" + + " lastName:\n" + + " type: string\n" + + " nullable: true\n" + + " address:\n" + + " required:\n" + + " - address2\n" + + " type: object\n" + + " properties:\n" + + " street:\n" + + " type: string\n" + + " city:\n" + + " type: string\n" + + " nullable: true\n" + + " address2:\n" + + " type: string\n" + + " department:\n" + + " required:\n" + + " - address\n" + + " - id\n" + + " type: object\n" + + " properties:\n" + + " id:\n" + + " type: integer\n" + + " format: int64\n" + + " address:\n" + + " required:\n" + + " - address2\n" + + " type: object\n" + + " properties:\n" + + " street:\n" + + " type: string\n" + + " city:\n" + + " type: string\n" + + " nullable: true\n" + + " address2:\n" + + " type: string\n" + + " responses:\n" + + " default:\n" + + " description: default response\n" + + " content:\n" + + " '*/*': {}\n" + + "components:\n" + + " schemas:\n" + + " Pet:\n" + + " required:\n" + + " - address2\n" + + " - street\n" + + " type: object\n" + + " properties:\n" + + " street:\n" + + " type: string\n" + + " city:\n" + + " type: string\n" + + " nullable: true\n" + + " address2:\n" + + " type: string\n" + + " Address:\n" + + " required:\n" + + " - address2\n" + + " type: object\n" + + " properties:\n" + + " street:\n" + + " type: string\n" + + " city:\n" + + " type: string\n" + + " nullable: true\n" + + " address2:\n" + + " type: string\n" + + " Department:\n" + + " required:\n" + + " - address\n" + + " - id\n" + + " type: object\n" + + " properties:\n" + + " id:\n" + + " type: integer\n" + + " format: int64\n" + + " address:\n" + + " required:\n" + + " - address2\n" + + " type: object\n" + + " properties:\n" + + " street:\n" + + " type: string\n" + + " city:\n" + + " type: string\n" + + " nullable: true\n" + + " address2:\n" + + " type: string\n" + + " Person:\n" + + " required:\n" + + " - address\n" + + " - department\n" + + " - firstName\n" + + " - id\n" + + " type: object\n" + + " properties:\n" + + " id:\n" + + " type: integer\n" + + " format: int64\n" + + " firstName:\n" + + " type: string\n" + + " lastName:\n" + + " type: string\n" + + " nullable: true\n" + + " address:\n" + + " required:\n" + + " - address2\n" + + " type: object\n" + + " properties:\n" + + " street:\n" + + " type: string\n" + + " city:\n" + + " type: string\n" + + " nullable: true\n" + + " address2:\n" + + " type: string\n" + + " department:\n" + + " required:\n" + + " - address\n" + + " - id\n" + + " type: object\n" + + " properties:\n" + + " id:\n" + + " type: integer\n" + + " format: int64\n" + + " address:\n" + + " required:\n" + + " - address2\n" + + " type: object\n" + + " properties:\n" + + " street:\n" + + " type: string\n" + + " city:\n" + + " type: string\n" + + " nullable: true\n" + + " address2:\n" + + " type: string\n"; + SerializationMatchers.assertEqualsToYaml(openAPI, yaml); + ModelConverters.reset(); + } } diff --git a/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/resources/Ticket4848ProcessorResource.java b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/resources/Ticket4848ProcessorResource.java new file mode 100644 index 0000000000..09c852338d --- /dev/null +++ b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/resources/Ticket4848ProcessorResource.java @@ -0,0 +1,21 @@ +package io.swagger.v3.jaxrs2.resources; + +import io.swagger.v3.jaxrs2.resources.jspecify.Pet; +import io.swagger.v3.jaxrs2.resources.jspecify.nullmarked.Person; + +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; + +@Path("/test") +public class Ticket4848ProcessorResource { + + @PUT + @Path("/updateperson") + public void putPerson(Person cart) {} + + @POST + @Path("/createpet") + public void postPet(Pet cart) {} + +} diff --git a/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/resources/jspecify/Pet.java b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/resources/jspecify/Pet.java new file mode 100644 index 0000000000..3c4d0844d5 --- /dev/null +++ b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/resources/jspecify/Pet.java @@ -0,0 +1,37 @@ +package io.swagger.v3.jaxrs2.resources.jspecify; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@NullMarked +public class Pet { + private String street; // in NullUnmarked package: unspecified nullness + private @Nullable String city; // explicitly nullable + + @NonNull + public String address2; + + public Pet() {} + + public Pet(String street, @Nullable String city) { + this.street = street; + this.city = city; + } + + public String getStreet() { + return street; + } + + public void setStreet(String street) { + this.street = street; + } + + public @Nullable String getCity() { + return city; + } + + public void setCity(@Nullable String city) { + this.city = city; + } +} diff --git a/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/resources/jspecify/nullmarked/Department.java b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/resources/jspecify/nullmarked/Department.java new file mode 100644 index 0000000000..8b0e48c200 --- /dev/null +++ b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/resources/jspecify/nullmarked/Department.java @@ -0,0 +1,25 @@ +package io.swagger.v3.jaxrs2.resources.jspecify.nullmarked; + +import io.swagger.v3.jaxrs2.resources.jspecify.nullunmarked.Address; + +public class Department { + private Long id; // default not-null due to @NullMarked on package + private Address address; // from NullUnmarked package -> may be nullable inside + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + + public Address getAddress() { + return address; + } + + public void setAddress(Address address) { + this.address = address; + } +} diff --git a/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/resources/jspecify/nullmarked/Person.java b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/resources/jspecify/nullmarked/Person.java new file mode 100644 index 0000000000..eb6debca1a --- /dev/null +++ b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/resources/jspecify/nullmarked/Person.java @@ -0,0 +1,47 @@ +package io.swagger.v3.jaxrs2.resources.jspecify.nullmarked; + +import io.swagger.v3.jaxrs2.resources.jspecify.nullunmarked.Address; +import org.jspecify.annotations.Nullable; + +public class Person { + private Long id; // default not-null due to @NullMarked on package + + private String firstName; // default not-null + private @Nullable String lastName; // explicitly nullable + + private Address address; // from NullUnmarked package -> may be nullable inside + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public @Nullable String getLastName() { + return lastName; + } + + public void setLastName(@Nullable String lastName) { + this.lastName = lastName; + } + + public Address getAddress() { + return address; + } + + public void setAddress(Address address) { + this.address = address; + } + + public Department department; +} diff --git a/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/resources/jspecify/nullmarked/package-info.java b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/resources/jspecify/nullmarked/package-info.java new file mode 100644 index 0000000000..ed6f7be37f --- /dev/null +++ b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/resources/jspecify/nullmarked/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package io.swagger.v3.jaxrs2.resources.jspecify.nullmarked; + +import org.jspecify.annotations.NullMarked; \ No newline at end of file diff --git a/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/resources/jspecify/nullunmarked/Address.java b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/resources/jspecify/nullunmarked/Address.java new file mode 100644 index 0000000000..750b35424a --- /dev/null +++ b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/resources/jspecify/nullunmarked/Address.java @@ -0,0 +1,35 @@ +package io.swagger.v3.jaxrs2.resources.jspecify.nullunmarked; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +public class Address { + private String street; // in NullUnmarked package: unspecified nullness + private @Nullable String city; // explicitly nullable + + @NonNull + public String address2; + + public Address() {} + + public Address(String street, @Nullable String city) { + this.street = street; + this.city = city; + } + + public String getStreet() { + return street; + } + + public void setStreet(String street) { + this.street = street; + } + + public @Nullable String getCity() { + return city; + } + + public void setCity(@Nullable String city) { + this.city = city; + } +} diff --git a/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/resources/jspecify/nullunmarked/package-info.java b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/resources/jspecify/nullunmarked/package-info.java new file mode 100644 index 0000000000..0a252d2f74 --- /dev/null +++ b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/resources/jspecify/nullunmarked/package-info.java @@ -0,0 +1,4 @@ +@NullUnmarked +package io.swagger.v3.jaxrs2.resources.jspecify.nullunmarked; + +import org.jspecify.annotations.NullUnmarked; \ No newline at end of file