Bug description
While input schema generation includes Jackson annotations like @JsonPropertyDescription, output schema generation does not. The root cause of this is JsonSchemaGenerator.internalGenerateFromClass creating its schema generator config from scratch instead of using TYPE_SCHEMA_GENERATOR or at least including the Jackson module manually.
Environment Spring AI 1.1.0-M2, reactive mode, Java 24.
Steps to reproduce
Use a @McpTool method with a return type that uses Jackson description annotations.
Expected behavior The Jackson annotations are included in the schema generation.
Minimal Complete Reproducible example
@SpringBootApplication
class Application {
companion object {
@JvmStatic
fun main(args: Array<String>) {
runApplication<Application>(*args)
}
}
data class Input(@JsonPropertyDescription("Test Input") val value: String)
data class Output(@JsonPropertyDescription("Test Output") val value: String)
@McpTool
fun test(@McpToolParam(description = "input") input: Input): Output {
return Output(input.value)
}
}
Tool listing response:
{
"name": "test",
"title": "test",
"description": "",
"inputSchema": {
"type": "object",
"properties": {
"input": {
"type": "object",
"properties": {
"value": {
"type": "string",
"description": "Test Input"
}
},
"required": [
"value"
],
"description": "input"
}
},
"required": [
"input"
]
},
"outputSchema": {
"type": "object",
"properties": {
"value": {
"type": "string"
}
},
"$schema": "https://json-schema.org/draft/2020-12/schema"
},
"annotations": {
"title": "",
"readOnlyHint": false,
"destructiveHint": true,
"idempotentHint": false,
"openWorldHint": true
}
}
Comment From: rnett
This is even worse than I thought, since optionality and nullability come from Jackson. A nullable type in your output schema will cause validation errors on the response, since the nullability isn't recorded in the schema.
Comment From: okohub
Looks like inputSchema and outputSchema generation differs in method calls. May be there is a reason behind this decision.
Input Schema: "generateForMethodInput" which is honoring descriptions
String inputSchema = JsonSchemaGenerator.generateForMethodInput(mcpToolMethod);
````
Output Schema: "generateFromType" which is more like honoring base structure.
```java
toolBuilder.outputSchema(this.getJsonMapper(),
JsonSchemaGenerator.generateFromType(mcpToolMethod.getGenericReturnType()));
We may agree on architectural decision (like outputSchema is only for validation), but nullability seems like a bug.
Comment From: HDHXTM
The framework uses org.springaicommunity.mcp.method.tool.utils.JsonSchemaGenerator#internalGenerateFromType to generate the output schema. code:
SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2020_12,
OptionPreset.PLAIN_JSON);
SchemaGeneratorConfig config = configBuilder.with(Option.EXTRA_OPEN_API_FORMAT_VALUES)
.without(Option.FLATTENED_ENUMS_FROM_TOSTRING)
.build();
SchemaGenerator generator = new SchemaGenerator(config);
JsonNode jsonSchema = generator.generateSchema(type);
return jsonSchema.toPrettyString();
It is my test code :
import cn.hutool.core.util.ReflectUtil;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import com.fasterxml.jackson.databind.JsonNode;
import com.github.victools.jsonschema.generator.*;
import lombok.Data;
import org.springaicommunity.mcp.method.tool.utils.JsonSchemaGenerator;
@Data
public class CourseInfoDto {
@JsonPropertyDescription("课程id")
private String id;
/**
* 课程名称
*/
@JsonPropertyDescription("课程名称")
private String courseName;
public static void main(String[] args) {
SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2020_12,
OptionPreset.PLAIN_JSON);
SchemaGeneratorConfigBuilder builder = configBuilder.with(Option.EXTRA_OPEN_API_FORMAT_VALUES)
.without(Option.FLATTENED_ENUMS_FROM_TOSTRING);
builder.forFields().withDescriptionResolver(a -> {
JsonPropertyDescription propertyAnnotation = a.getAnnotationConsideringFieldAndGetterIfSupported(JsonPropertyDescription.class);
if (propertyAnnotation != null) {
return propertyAnnotation.value();
}
return null;
});
SchemaGeneratorConfig config = builder.build();
SchemaGenerator generator = new SchemaGenerator(config);
JsonNode jsonSchema = generator.generateSchema(CourseInfoDto.class);
System.out.println(jsonSchema.toPrettyString());
JsonSchemaGenerator g = new JsonSchemaGenerator();
SchemaGenerator schemaGenerator = (SchemaGenerator) ReflectUtil.getFieldValue(g, "TYPE_SCHEMA_GENERATOR");
System.out.println(schemaGenerator.generateSchema(CourseInfoDto.class).toPrettyString());
}
}
Running my code produces the following output:
{
"$schema" : "https://json-schema.org/draft/2020-12/schema",
"type" : "object",
"properties" : {
"courseName" : {
"type" : "string",
"description" : "课程名称"
},
"id" : {
"type" : "string",
"description" : "课程id"
}
}
}
{
"$schema" : "https://json-schema.org/draft/2020-12/schema",
"type" : "object",
"properties" : {
"courseName" : {
"type" : "string",
"description" : "课程名称"
},
"id" : {
"type" : "string",
"description" : "课程id"
}
},
"required" : [ "courseName", "id" ]
}
So why doesn't the author use org.springaicommunity.mcp.method.tool.utils.JsonSchemaGenerator#TYPE_SCHEMA_GENERATOR to generate the output schema?
Comment From: SuperZ1999
This is even worse than I thought, since optionality and nullability come from Jackson. A nullable type in your output schema will cause validation errors on the response, since the nullability isn't recorded in the schema.
@rnett Hello, have you resolved this? I'm facing the same problem.
Comment From: rnett
I have not, I switched to the Kotlin SDK (which lets and makes you generate the output schema yourself).