fix(schema): remove AddNullable from draft2020_12 settings (#664)

* fix(schema): remove AddNullable from draft2020_12 settings

The `nullable` keyword is an OpenAPI 3.0 extension, not part of
JSON Schema 2020-12. Using AddNullable with draft2020_12 settings
causes validation failures with strict JSON Schema validators.

JSON Schema 2020-12 represents nullable types using:
- {"type": ["string", "null"]} (type array with null)
- {"anyOf": [{"type": "string"}, {"type": "null"}]}

Fixes #663

* test(schema): update complex schema nullable expectation

* test(schema): align macro optional-field expectations with draft2020
This commit is contained in:
Mark Wotton 2026-02-19 23:19:45 +07:00 • committed by GitHub
commit 92b1459647
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 24 additions and 21 deletions

View file

@ -23,8 +23,10 @@ pub fn schema_for_type<T: JsonSchema + std::any::Any>() -> Arc<JsonObject> {
} else {
// explicitly to align json schema version to official specifications.
// refer to https://github.com/modelcontextprotocol/modelcontextprotocol/pull/655 for details.
let mut settings = SchemaSettings::draft2020_12();
settings.transforms = vec![Box::new(schemars::transform::AddNullable::default())];
let settings = SchemaSettings::draft2020_12();
// Note: AddNullable is intentionally NOT used here because the `nullable` keyword
// is an OpenAPI 3.0 extension, not part of JSON Schema 2020-12. Using it would
// cause validation failures with strict JSON Schema validators.
let generator = settings.into_generator();
let schema = generator.into_root_schema_for::<T>();
let object = serde_json::to_value(schema).expect("failed to serialize schema");

View file

@ -80,8 +80,10 @@ fn expected_schema() -> serde_json::Value {
"type": "array"
},
"system": {
"nullable": true,
"type": "string"
"type": [
"string",
"null"
]
}
},
"required": [

View file

@ -238,7 +238,7 @@ fn test_optional_field_schema_generation_via_macro() {
);
// Verify the schema generated for the aggregated OptionalFieldTestSchema
// by the macro infrastructure (which should now use OpenAPI 3 settings)
// by the macro infrastructure using JSON Schema 2020-12 settings.
let input_schema_map = &*tool_attr.input_schema; // Dereference Arc<JsonObject>
// Check the schema for the 'description' property within the input schema
@ -253,18 +253,21 @@ fn test_optional_field_schema_generation_via_macro() {
.as_object()
.unwrap();
// Assert that the format is now `type: "string", nullable: true`
// Assert nullable Option<T> is represented in JSON Schema 2020-12 form.
let type_value = description_schema
.get("type")
.expect("Schema for Option<String> should include a type field");
let type_array = type_value
.as_array()
.expect("Schema for Option<String> should use a type array [T, null]");
assert_eq!(
description_schema.get("type").map(|v| v.as_str().unwrap()),
Some("string"),
"Schema for Option<String> generated by macro should be type: \"string\""
type_array,
&vec![serde_json::json!("string"), serde_json::json!("null")],
"Schema for Option<String> should be type: [\"string\", \"null\"]"
);
assert_eq!(
description_schema
.get("nullable")
.map(|v| v.as_bool().unwrap()),
Some(true),
"Schema for Option<String> generated by macro should have nullable: true"
assert!(
description_schema.get("nullable").is_none(),
"Schema for Option<String> should not use OpenAPI nullable in JSON Schema 2020-12"
);
// We still check the description is correct
assert_eq!(
@ -274,12 +277,8 @@ fn test_optional_field_schema_generation_via_macro() {
Some("An optional description field")
);
// Ensure the old 'type: [T, null]' format is NOT used
let type_value = description_schema.get("type").unwrap();
assert!(
!type_value.is_array(),
"Schema type should not be an array [T, null]"
);
// Ensure no OpenAPI-only nullable extension was emitted.
assert!(description_schema.get("nullable").is_none());
}
// Define a dummy client handler