From 3a2d19fa08166f69146918ea40b1e6485d520db7 Mon Sep 17 00:00:00 2001 From: Sanjay Santhanam <51058514+Sanjays2402@users.noreply.github.com> Date: Sun, 13 Sep 2026 16:28:24 -0700 Subject: [PATCH 1/2] Fix build_model_schema putting pydantic constraint objects into field descriptions FieldInfo.metadata is a list of pydantic constraint objects (Ge, Le, ...); the builder used metadata[0] as the field description, making the schema not JSON-serializable and dropping the real description. Use field_info.description instead, and add a regression test. --- .../schema/kernel_json_schema_builder.py | 5 +++-- python/tests/unit/schema/test_schema_builder.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/python/semantic_kernel/schema/kernel_json_schema_builder.py b/python/semantic_kernel/schema/kernel_json_schema_builder.py index 5ec519b5b377..fe44bdf97d45 100644 --- a/python/semantic_kernel/schema/kernel_json_schema_builder.py +++ b/python/semantic_kernel/schema/kernel_json_schema_builder.py @@ -91,9 +91,10 @@ def build_model_schema( field_info = model.model_fields[field_name] if isinstance(field_info.metadata, dict): field_description = field_info.metadata.get("description") - elif isinstance(field_info.metadata, list) and field_info.metadata: - field_description = field_info.metadata[0] elif hasattr(field_info, "description"): + # Pydantic v2 keeps constraints (Ge, Le, ...) in field_info.metadata, + # which is a list of constraint objects and not a description, + # so the description must come from field_info.description instead. field_description = field_info.description if not cls._is_optional(field_type): required.append(field_name) diff --git a/python/tests/unit/schema/test_schema_builder.py b/python/tests/unit/schema/test_schema_builder.py index 5d24a599c96c..8a6bc13ec093 100644 --- a/python/tests/unit/schema/test_schema_builder.py +++ b/python/tests/unit/schema/test_schema_builder.py @@ -6,6 +6,7 @@ from unittest.mock import Mock import pytest +from pydantic import Field from semantic_kernel.connectors.utils.structured_output_schema import generate_structured_output_response_format_schema from semantic_kernel.kernel_pydantic import KernelBaseModel @@ -455,3 +456,16 @@ def test_build_schema_with_nonpydantic_structured_output(): } assert structured_output_schema == expected_schema + + +class ConstrainedParams(KernelBaseModel): + top_p: float = Field(default=0.9, ge=0.0, le=1.0, description="nucleus sampling") + + +def test_build_model_schema_with_field_constraints_keeps_description(): + # Regression test for https://github.com/microsoft/semantic-kernel/issues/14443: + # pydantic constraint objects (Ge, Le, ...) live in FieldInfo.metadata and + # must not end up in the field description. + schema = KernelJsonSchemaBuilder.build_model_schema(ConstrainedParams) + assert schema["properties"]["top_p"]["description"] == "nucleus sampling" + json.dumps(schema) # the schema must be JSON serializable From 00bd4f596612e037609387ef75b834f3edaf787f Mon Sep 17 00:00:00 2001 From: Sanjay Santhanam <51058514+Sanjays2402@users.noreply.github.com> Date: Sun, 4 Oct 2026 12:00:40 -0700 Subject: [PATCH 2/2] Preserve Annotated description metadata without serializing constraints --- .../schema/kernel_json_schema_builder.py | 18 ++++++----- .../tests/unit/schema/test_schema_builder.py | 31 +++++++++++++++++++ 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/python/semantic_kernel/schema/kernel_json_schema_builder.py b/python/semantic_kernel/schema/kernel_json_schema_builder.py index fe44bdf97d45..5d5933088c22 100644 --- a/python/semantic_kernel/schema/kernel_json_schema_builder.py +++ b/python/semantic_kernel/schema/kernel_json_schema_builder.py @@ -89,13 +89,17 @@ def build_model_schema( field_description = None if hasattr(model, "model_fields") and field_name in model.model_fields: field_info = model.model_fields[field_name] - if isinstance(field_info.metadata, dict): - field_description = field_info.metadata.get("description") - elif hasattr(field_info, "description"): - # Pydantic v2 keeps constraints (Ge, Le, ...) in field_info.metadata, - # which is a list of constraint objects and not a description, - # so the description must come from field_info.description instead. - field_description = field_info.description + field_description = getattr(field_info, "description", None) + if field_description is None: + metadata = field_info.metadata + if isinstance(metadata, dict): + field_description = metadata.get("description") + elif isinstance(metadata, (list, tuple)): + for entry in metadata: + candidate = entry.get("description") if isinstance(entry, dict) else entry + if isinstance(candidate, str): + field_description = candidate + break if not cls._is_optional(field_type): required.append(field_name) properties[field_name] = cls.build(field_type, field_description, structured_output) diff --git a/python/tests/unit/schema/test_schema_builder.py b/python/tests/unit/schema/test_schema_builder.py index 8a6bc13ec093..89594a110aef 100644 --- a/python/tests/unit/schema/test_schema_builder.py +++ b/python/tests/unit/schema/test_schema_builder.py @@ -469,3 +469,34 @@ def test_build_model_schema_with_field_constraints_keeps_description(): schema = KernelJsonSchemaBuilder.build_model_schema(ConstrainedParams) assert schema["properties"]["top_p"]["description"] == "nucleus sampling" json.dumps(schema) # the schema must be JSON serializable + + +@pytest.mark.parametrize("metadata", ["Annotated description", {"description": "Annotated description"}]) +@pytest.mark.parametrize("container", [list, tuple]) +def test_build_model_schema_with_annotated_description(metadata, container): + class Params(KernelBaseModel): + value: Annotated[float, Field(ge=0), metadata] + + field = Params.model_fields["value"] + field.metadata = container(field.metadata) + schema = KernelJsonSchemaBuilder.build_model_schema(Params) + assert schema["properties"]["value"]["description"] == "Annotated description" + json.dumps(schema) + + +def test_build_model_schema_explicit_description_overrides_metadata(): + class Params(KernelBaseModel): + value: Annotated[float, "Fallback description", Field(ge=0, description="Explicit description")] + + schema = KernelJsonSchemaBuilder.build_model_schema(Params) + assert schema["properties"]["value"]["description"] == "Explicit description" + json.dumps(schema) + + +def test_build_model_schema_ignores_non_description_metadata(): + class Params(KernelBaseModel): + value: Annotated[float, Field(ge=0), {"description": 123}] + + schema = KernelJsonSchemaBuilder.build_model_schema(Params) + assert "description" not in schema["properties"]["value"] + json.dumps(schema)