Skip to content

Commit 5c9cf29

Browse files
authored
Fix Pydantic field alias consistency in structured output (#1099)
1 parent 906ceea commit 5c9cf29

File tree

2 files changed

+46
-1
lines changed

2 files changed

+46
-1
lines changed

src/mcp/server/fastmcp/utilities/func_metadata.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ def convert_result(self, result: Any) -> Any:
111111

112112
assert self.output_model is not None, "Output model must be set if output schema is defined"
113113
validated = self.output_model.model_validate(result)
114-
structured_content = validated.model_dump(mode="json")
114+
structured_content = validated.model_dump(mode="json", by_alias=True)
115115

116116
return (unstructured_content, structured_content)
117117

tests/server/fastmcp/test_func_metadata.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -839,3 +839,48 @@ def func_returning_namedtuple() -> Point:
839839
func_metadata(func_returning_namedtuple, structured_output=True)
840840
assert "is not serializable for structured output" in str(exc_info.value)
841841
assert "Point" in str(exc_info.value)
842+
843+
844+
def test_structured_output_aliases():
845+
"""Test that field aliases are consistent between schema and output"""
846+
847+
class ModelWithAliases(BaseModel):
848+
field_first: str | None = Field(default=None, alias="first", description="The first field.")
849+
field_second: str | None = Field(default=None, alias="second", description="The second field.")
850+
851+
def func_with_aliases() -> ModelWithAliases:
852+
# When aliases are defined, we must use the aliased names to set values
853+
return ModelWithAliases(**{"first": "hello", "second": "world"})
854+
855+
meta = func_metadata(func_with_aliases)
856+
857+
# Check that schema uses aliases
858+
assert meta.output_schema is not None
859+
assert "first" in meta.output_schema["properties"]
860+
assert "second" in meta.output_schema["properties"]
861+
assert "field_first" not in meta.output_schema["properties"]
862+
assert "field_second" not in meta.output_schema["properties"]
863+
864+
# Check that the actual output uses aliases too
865+
result = ModelWithAliases(**{"first": "hello", "second": "world"})
866+
unstructured_content, structured_content = meta.convert_result(result)
867+
868+
# The structured content should use aliases to match the schema
869+
assert "first" in structured_content
870+
assert "second" in structured_content
871+
assert "field_first" not in structured_content
872+
assert "field_second" not in structured_content
873+
assert structured_content["first"] == "hello"
874+
assert structured_content["second"] == "world"
875+
876+
# Also test the case where we have a model with defaults to ensure aliases work in all cases
877+
result_with_defaults = ModelWithAliases() # Uses default None values
878+
unstructured_content_defaults, structured_content_defaults = meta.convert_result(result_with_defaults)
879+
880+
# Even with defaults, should use aliases in output
881+
assert "first" in structured_content_defaults
882+
assert "second" in structured_content_defaults
883+
assert "field_first" not in structured_content_defaults
884+
assert "field_second" not in structured_content_defaults
885+
assert structured_content_defaults["first"] is None
886+
assert structured_content_defaults["second"] is None

0 commit comments

Comments
 (0)