Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions python/semantic_kernel/schema/kernel_json_schema_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,20 @@ 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 isinstance(field_info.metadata, list) and field_info.metadata:
field_description = field_info.metadata[0]
elif hasattr(field_info, "description"):
# `FieldInfo.metadata` holds constraint objects (e.g. annotated_types.Ge)
# alongside any description metadata supplied through Annotated,
# so only strings and dicts can be descriptions; never use
# `metadata[0]` blindly as it is most often a constraint.
field_metadata = getattr(field_info, "metadata", None)
if isinstance(field_metadata, (list, tuple)):
for meta in field_metadata:
if isinstance(meta, str):
field_description = meta
break
if isinstance(meta, dict) and meta.get("description"):
field_description = meta["description"]
break
if field_description is None and field_info.description:
field_description = field_info.description
if not cls._is_optional(field_type):
required.append(field_name)
Expand Down
38 changes: 38 additions & 0 deletions python/tests/unit/schema/test_schema_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -455,3 +455,41 @@ def test_build_schema_with_nonpydantic_structured_output():
}

assert structured_output_schema == expected_schema


def test_build_model_schema_field_description_with_constraints():
"""Field descriptions must survive when the field also has constraints.

Pydantic stores constraints (e.g. `ge=0`) in `FieldInfo.metadata`; taking
`metadata[0]` as the description leaks the constraint object into the
schema, which then cannot be JSON serialized when sent to the model.
"""
from pydantic import Field

class ModelWithConstraints(KernelBaseModel):
count: int = Field(description="number of items", ge=0)
ratio: float = Field(description="the ratio", gt=0, le=1)
label: str = Field(description="the label")

schema = KernelJsonSchemaBuilder.build(ModelWithConstraints)

assert schema["properties"]["count"]["description"] == "number of items"
assert schema["properties"]["ratio"]["description"] == "the ratio"
assert schema["properties"]["label"]["description"] == "the label"
# the full schema must stay JSON serializable (function calling payload)
assert json.loads(json.dumps(schema))


def test_build_model_schema_annotated_descriptions():
"""Descriptions supplied via Annotated metadata must be used, also with constraints."""
from pydantic import Field

class ModelWithAnnotated(KernelBaseModel):
amount: Annotated[int, "amount description"] = Field(ge=1)
tagged: Annotated[str, {"description": "tagged description"}] = "x"

schema = KernelJsonSchemaBuilder.build(ModelWithAnnotated)

assert schema["properties"]["amount"]["description"] == "amount description"
assert schema["properties"]["tagged"]["description"] == "tagged description"
assert json.loads(json.dumps(schema))
Loading