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 { } else {
// explicitly to align json schema version to official specifications. // explicitly to align json schema version to official specifications.
// refer to https://github.com/modelcontextprotocol/modelcontextprotocol/pull/655 for details. // refer to https://github.com/modelcontextprotocol/modelcontextprotocol/pull/655 for details.
let mut settings = SchemaSettings::draft2020_12(); let settings = SchemaSettings::draft2020_12();
settings.transforms = vec![Box::new(schemars::transform::AddNullable::default())]; // 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 generator = settings.into_generator();
let schema = generator.into_root_schema_for::<T>(); let schema = generator.into_root_schema_for::<T>();
let object = serde_json::to_value(schema).expect("failed to serialize schema"); 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" "type": "array"
}, },
"system": { "system": {
"nullable": true, "type": [
"type": "string" "string",
"null"
]
} }
}, },
"required": [ "required": [

View file

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