diff --git a/spring-ai-model/pom.xml b/spring-ai-model/pom.xml
index 70874f2d865..f374c0a6c40 100644
--- a/spring-ai-model/pom.xml
+++ b/spring-ai-model/pom.xml
@@ -154,6 +154,13 @@
test
+
+ com.google.code.findbugs
+ jsr305
+ 3.0.2
+ test
+
+
diff --git a/spring-ai-model/src/main/java/org/springframework/ai/util/json/schema/JsonSchemaGenerator.java b/spring-ai-model/src/main/java/org/springframework/ai/util/json/schema/JsonSchemaGenerator.java
index 27fae0fcc55..6ddb656e6f1 100644
--- a/spring-ai-model/src/main/java/org/springframework/ai/util/json/schema/JsonSchemaGenerator.java
+++ b/spring-ai-model/src/main/java/org/springframework/ai/util/json/schema/JsonSchemaGenerator.java
@@ -20,6 +20,7 @@
import java.lang.reflect.Parameter;
import java.lang.reflect.Type;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
@@ -213,8 +214,9 @@ private static boolean isMethodParameterRequired(Method method, int index) {
|| schemaAnnotation.requiredMode() == Schema.RequiredMode.AUTO || schemaAnnotation.required();
}
- var nullableAnnotation = parameter.getAnnotation(Nullable.class);
- if (nullableAnnotation != null) {
+ var nullableAnnotationPresent = Arrays.stream(parameter.getAnnotations())
+ .anyMatch(ann -> ann.annotationType().getSimpleName().equals("Nullable"));
+ if (nullableAnnotationPresent) {
return false;
}
diff --git a/spring-ai-model/src/main/java/org/springframework/ai/util/json/schema/SpringAiSchemaModule.java b/spring-ai-model/src/main/java/org/springframework/ai/util/json/schema/SpringAiSchemaModule.java
index 2182fc6b25b..bf23127c6ea 100644
--- a/spring-ai-model/src/main/java/org/springframework/ai/util/json/schema/SpringAiSchemaModule.java
+++ b/spring-ai-model/src/main/java/org/springframework/ai/util/json/schema/SpringAiSchemaModule.java
@@ -16,6 +16,7 @@
package org.springframework.ai.util.json.schema;
+import java.lang.annotation.Annotation;
import java.util.stream.Stream;
import com.fasterxml.jackson.annotation.JsonProperty;
@@ -104,7 +105,8 @@ private boolean checkRequired(MemberScope, ?> member) {
|| schemaAnnotation.requiredMode() == Schema.RequiredMode.AUTO || schemaAnnotation.required();
}
- var nullableAnnotation = member.getAnnotationConsideringFieldAndGetter(Nullable.class);
+ var nullableAnnotation = member.getAnnotationConsideringFieldAndGetter(Annotation.class,
+ ann -> ann.annotationType().getSimpleName().equals("Nullable"));
if (nullableAnnotation != null) {
return false;
}
diff --git a/spring-ai-model/src/test/java/org/springframework/ai/util/json/JsonSchemaGeneratorTests.java b/spring-ai-model/src/test/java/org/springframework/ai/util/json/JsonSchemaGeneratorTests.java
index c0c61f7eab6..3845d399676 100644
--- a/spring-ai-model/src/test/java/org/springframework/ai/util/json/JsonSchemaGeneratorTests.java
+++ b/spring-ai-model/src/test/java/org/springframework/ai/util/json/JsonSchemaGeneratorTests.java
@@ -34,7 +34,6 @@
import org.springframework.ai.chat.model.ToolContext;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.ai.util.json.schema.JsonSchemaGenerator;
-import org.springframework.lang.Nullable;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -216,6 +215,171 @@ void generateSchemaForMethodWithNullableAnnotations() throws Exception {
assertThat(schema).isEqualToIgnoringWhitespace(expectedJsonSchema);
}
+ @Test
+ void generateSchemaForMethodWithNullableAnnotations_FromJavax() throws Exception {
+ Method method = TestMethods.class.getDeclaredMethod("nullableMethod_FromJavax", String.class, String.class);
+
+ String schema = JsonSchemaGenerator.generateForMethodInput(method);
+ String expectedJsonSchema = """
+ {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "type": "object",
+ "properties": {
+ "username": {
+ "type": "string"
+ },
+ "password": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "password"
+ ],
+ "additionalProperties": false
+ }
+ """;
+
+ assertThat(schema).isEqualToIgnoringWhitespace(expectedJsonSchema);
+ }
+
+ @Test
+ void generateSchemaForMethodWithNullableAnnotations_FromJakarta() throws Exception {
+ Method method = TestMethods.class.getDeclaredMethod("nullableMethod_FromJakarta", String.class, String.class);
+
+ String schema = JsonSchemaGenerator.generateForMethodInput(method);
+ String expectedJsonSchema = """
+ {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "type": "object",
+ "properties": {
+ "username": {
+ "type": "string"
+ },
+ "password": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "password"
+ ],
+ "additionalProperties": false
+ }
+ """;
+
+ assertThat(schema).isEqualToIgnoringWhitespace(expectedJsonSchema);
+ }
+
+ @Test
+ void generateSchemaForMethodWithNullableAnnotations_FromReactor() throws Exception {
+ Method method = TestMethods.class.getDeclaredMethod("nullableMethod_FromReactor", String.class, String.class);
+
+ String schema = JsonSchemaGenerator.generateForMethodInput(method);
+ String expectedJsonSchema = """
+ {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "type": "object",
+ "properties": {
+ "username": {
+ "type": "string"
+ },
+ "password": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "password"
+ ],
+ "additionalProperties": false
+ }
+ """;
+
+ assertThat(schema).isEqualToIgnoringWhitespace(expectedJsonSchema);
+ }
+
+ @Test
+ void generateSchemaForMethodWithNullableAnnotations_FromMicrometerContext() throws Exception {
+ Method method = TestMethods.class.getDeclaredMethod("nullableMethod_FromMicrometerContext", String.class,
+ String.class);
+
+ String schema = JsonSchemaGenerator.generateForMethodInput(method);
+ String expectedJsonSchema = """
+ {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "type": "object",
+ "properties": {
+ "username": {
+ "type": "string"
+ },
+ "password": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "password"
+ ],
+ "additionalProperties": false
+ }
+ """;
+
+ assertThat(schema).isEqualToIgnoringWhitespace(expectedJsonSchema);
+ }
+
+ @Test
+ void generateSchemaForMethodWithNullableAnnotations_FromMicrometerCommonLang() throws Exception {
+ Method method = TestMethods.class.getDeclaredMethod("nullableMethod_FromMicrometerCommonLang", String.class,
+ String.class);
+
+ String schema = JsonSchemaGenerator.generateForMethodInput(method);
+ String expectedJsonSchema = """
+ {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "type": "object",
+ "properties": {
+ "username": {
+ "type": "string"
+ },
+ "password": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "password"
+ ],
+ "additionalProperties": false
+ }
+ """;
+
+ assertThat(schema).isEqualToIgnoringWhitespace(expectedJsonSchema);
+ }
+
+ @Test
+ void generateSchemaForMethodWithNullableAnnotations_FromMicrometerCoreLang() throws Exception {
+ Method method = TestMethods.class.getDeclaredMethod("nullableMethod_FromMicrometerCoreLang", String.class,
+ String.class);
+
+ String schema = JsonSchemaGenerator.generateForMethodInput(method);
+ String expectedJsonSchema = """
+ {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "type": "object",
+ "properties": {
+ "username": {
+ "type": "string"
+ },
+ "password": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "password"
+ ],
+ "additionalProperties": false
+ }
+ """;
+
+ assertThat(schema).isEqualToIgnoringWhitespace(expectedJsonSchema);
+ }
+
@Test
void generateSchemaForMethodWithAdditionalPropertiesAllowed() throws Exception {
Method method = TestMethods.class.getDeclaredMethod("simpleMethod", String.class, int.class);
@@ -542,7 +706,187 @@ void generateSchemaForTypeWithJacksonAnnotation() {
@Test
void generateSchemaForTypeWithNullableAnnotation() {
- String schema = JsonSchemaGenerator.generateForType(JacksonPerson.class);
+ String schema = JsonSchemaGenerator.generateForType(NullablePerson.class);
+ String expectedJsonSchema = """
+ {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "type": "object",
+ "properties": {
+ "email": {
+ "type": "string"
+ },
+ "id": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "name"
+ ],
+ "additionalProperties": false
+ }
+ """;
+
+ assertThat(schema).isEqualToIgnoringWhitespace(expectedJsonSchema);
+ }
+
+ @Test
+ void generateSchemaForTypeWithNullableAnnotation_FromJavax() {
+ String schema = JsonSchemaGenerator.generateForType(NullablePerson_FromJavax.class);
+ String expectedJsonSchema = """
+ {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "type": "object",
+ "properties": {
+ "email": {
+ "type": "string"
+ },
+ "id": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "name"
+ ],
+ "additionalProperties": false
+ }
+ """;
+
+ assertThat(schema).isEqualToIgnoringWhitespace(expectedJsonSchema);
+ }
+
+ @Test
+ void generateSchemaForTypeWithNullableAnnotation_FromJakarta() {
+ String schema = JsonSchemaGenerator.generateForType(NullablePerson_FromJakarta.class);
+ String expectedJsonSchema = """
+ {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "type": "object",
+ "properties": {
+ "email": {
+ "type": "string"
+ },
+ "id": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "name"
+ ],
+ "additionalProperties": false
+ }
+ """;
+
+ assertThat(schema).isEqualToIgnoringWhitespace(expectedJsonSchema);
+ }
+
+ @Test
+ void generateSchemaForTypeWithNullableAnnotation_FromReactor() {
+ String schema = JsonSchemaGenerator.generateForType(NullablePerson_FromReactor.class);
+ String expectedJsonSchema = """
+ {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "type": "object",
+ "properties": {
+ "email": {
+ "type": "string"
+ },
+ "id": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "name"
+ ],
+ "additionalProperties": false
+ }
+ """;
+
+ assertThat(schema).isEqualToIgnoringWhitespace(expectedJsonSchema);
+ }
+
+ @Test
+ void generateSchemaForTypeWithNullableAnnotation_FromMicrometerContext() {
+ String schema = JsonSchemaGenerator.generateForType(NullablePerson_FromMicrometerContext.class);
+ String expectedJsonSchema = """
+ {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "type": "object",
+ "properties": {
+ "email": {
+ "type": "string"
+ },
+ "id": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "name"
+ ],
+ "additionalProperties": false
+ }
+ """;
+
+ assertThat(schema).isEqualToIgnoringWhitespace(expectedJsonSchema);
+ }
+
+ @Test
+ void generateSchemaForTypeWithNullableAnnotation_FromMicrometerCommonLang() {
+ String schema = JsonSchemaGenerator.generateForType(NullablePerson_FromMicrometerCommonLang.class);
+ String expectedJsonSchema = """
+ {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "type": "object",
+ "properties": {
+ "email": {
+ "type": "string"
+ },
+ "id": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "name"
+ ],
+ "additionalProperties": false
+ }
+ """;
+
+ assertThat(schema).isEqualToIgnoringWhitespace(expectedJsonSchema);
+ }
+
+ @Test
+ void generateSchemaForTypeWithNullableAnnotation_FromMicrometerCoreLang() {
+ String schema = JsonSchemaGenerator.generateForType(NullablePerson_FromMicrometerCoreLang.class);
String expectedJsonSchema = """
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
@@ -681,7 +1025,28 @@ public void jacksonMethod(
@JsonProperty(required = true) String password) {
}
- public void nullableMethod(@Nullable String username, String password) {
+ public void nullableMethod(@org.springframework.lang.Nullable String username, String password) {
+ }
+
+ public void nullableMethod_FromJavax(@javax.annotation.Nullable String username, String password) {
+ }
+
+ public void nullableMethod_FromJakarta(@jakarta.annotation.Nullable String username, String password) {
+ }
+
+ public void nullableMethod_FromReactor(@reactor.util.annotation.Nullable String username, String password) {
+ }
+
+ public void nullableMethod_FromMicrometerContext(@io.micrometer.context.Nullable String username,
+ String password) {
+ }
+
+ public void nullableMethod_FromMicrometerCommonLang(@io.micrometer.common.lang.Nullable String username,
+ String password) {
+ }
+
+ public void nullableMethod_FromMicrometerCoreLang(@io.micrometer.core.lang.Nullable String username,
+ String password) {
}
public void complexMethod(List items, TestData data, MoreTestData moreData) {
@@ -721,7 +1086,32 @@ record OpenApiPerson(@Schema(requiredMode = Schema.RequiredMode.REQUIRED) int id
}
- record NullablePerson(int id, String name, @Nullable String email) {
+ record NullablePerson(int id, String name, @org.springframework.lang.Nullable String email) {
+
+ }
+
+ record NullablePerson_FromJavax(int id, String name, @javax.annotation.Nullable String email) {
+
+ }
+
+ record NullablePerson_FromJakarta(int id, String name, @jakarta.annotation.Nullable String email) {
+
+ }
+
+ record NullablePerson_FromReactor(int id, String name, @reactor.util.annotation.Nullable String email) {
+
+ }
+
+ record NullablePerson_FromMicrometerContext(int id, String name, @io.micrometer.context.Nullable String email) {
+
+ }
+
+ record NullablePerson_FromMicrometerCommonLang(int id, String name,
+ @io.micrometer.common.lang.Nullable String email) {
+
+ }
+
+ record NullablePerson_FromMicrometerCoreLang(int id, String name, @io.micrometer.core.lang.Nullable String email) {
}