From 4007fd3509342c18a8c459975652bad31585e3dd Mon Sep 17 00:00:00 2001 From: Pushpak <200882761+Pushpak731@users.noreply.github.com> Date: Sat, 3 Oct 2026 00:16:22 +0530 Subject: [PATCH 1/2] Python: Exclude ClassVar annotations from generated JSON schemas KernelJsonSchemaBuilder.build_model_schema builds the instance schema from get_type_hints, which includes ClassVar annotations, so a class constant became a required object-valued property. Pydantic excludes ClassVars from model fields and serialized instances, so the generated schema rejected valid payloads (a serialized {'value': 1} failed validation because 'marker' was required). The builder now skips ClassVar-annotated hints (both the parameterized and bare forms, including inherited ones), for KernelBaseModel subclasses, plain annotated classes and instances, covering KernelParameterMetadata, generated tool definitions and structured_output schemas. Fixes #14527 --- .../schema/kernel_json_schema_builder.py | 7 +- .../tests/unit/schema/test_schema_builder.py | 103 +++++++++++++++++- 2 files changed, 108 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..f920e5745f11 100644 --- a/python/semantic_kernel/schema/kernel_json_schema_builder.py +++ b/python/semantic_kernel/schema/kernel_json_schema_builder.py @@ -3,7 +3,7 @@ import sys import types from enum import Enum -from typing import Any, Union, get_args, get_origin, get_type_hints +from typing import Any, ClassVar, Union, get_args, get_origin, get_type_hints from semantic_kernel.const import PARSED_ANNOTATION_UNION_DELIMITER from semantic_kernel.exceptions.function_exceptions import FunctionInvalidParameterConfiguration @@ -86,6 +86,11 @@ def build_model_schema( hints = get_type_hints(model, globalns=model_module_globals, localns={}) for field_name, field_type in hints.items(): + if field_type is ClassVar or get_origin(field_type) is ClassVar: + # ClassVar annotations are class-level constants: pydantic + # excludes them from model fields and serialized instances, + # so they must not become (required) schema properties. + continue field_description = None if hasattr(model, "model_fields") and field_name in model.model_fields: field_info = model.model_fields[field_name] diff --git a/python/tests/unit/schema/test_schema_builder.py b/python/tests/unit/schema/test_schema_builder.py index 5d24a599c96c..7f39cdca9786 100644 --- a/python/tests/unit/schema/test_schema_builder.py +++ b/python/tests/unit/schema/test_schema_builder.py @@ -2,7 +2,7 @@ import json from enum import Enum -from typing import Annotated, Any, Optional, Union +from typing import Annotated, Any, ClassVar, Optional, Union from unittest.mock import Mock import pytest @@ -455,3 +455,104 @@ def test_build_schema_with_nonpydantic_structured_output(): } assert structured_output_schema == expected_schema + + +class ModelWithClassVar(KernelBaseModel): + marker: ClassVar[str] = "constant" + value: int + + +class ModelWithBareClassVar(KernelBaseModel): + marker: ClassVar = 0 + value: str + + +class ModelInheritingClassVar(ModelWithClassVar): + extra: float + + +class PlainClassWithClassVar: + marker: ClassVar[dict] = {} + name: str + + +class ModelWithClassVarAndOptional(KernelBaseModel): + counter: ClassVar[int] = 5 + keep: str + drop: Optional[str] = None + + +def test_build_model_schema_excludes_class_var(): + schema = KernelJsonSchemaBuilder.build(ModelWithClassVar) + + assert schema == {"type": "object", "properties": {"value": {"type": "integer"}}, "required": ["value"]} + + +def test_build_model_schema_excludes_bare_class_var(): + schema = KernelJsonSchemaBuilder.build(ModelWithBareClassVar) + + assert schema == {"type": "object", "properties": {"value": {"type": "string"}}, "required": ["value"]} + + +def test_build_model_schema_excludes_inherited_class_var(): + schema = KernelJsonSchemaBuilder.build(ModelInheritingClassVar) + + assert schema == { + "type": "object", + "properties": {"value": {"type": "integer"}, "extra": {"type": "number"}}, + "required": ["value", "extra"], + } + + +def test_build_model_schema_excludes_class_var_on_plain_class(): + schema = KernelJsonSchemaBuilder.build(PlainClassWithClassVar) + + assert schema == {"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]} + + +def test_build_model_schema_class_var_with_optional_fields(): + schema = KernelJsonSchemaBuilder.build(ModelWithClassVarAndOptional) + + assert schema == { + "type": "object", + "properties": {"keep": {"type": "string"}, "drop": {"type": ["string", "null"]}}, + "required": ["keep"], + } + + +def test_build_model_schema_class_var_structured_output(): + schema = KernelJsonSchemaBuilder.build(ModelWithClassVar, structured_output=True) + + assert schema == { + "type": "object", + "properties": {"value": {"type": "integer"}}, + "required": ["value"], + "additionalProperties": False, + } + + +def test_build_model_schema_class_var_with_description(): + schema = KernelJsonSchemaBuilder.build(ModelWithClassVar, description="A payload") + + assert schema == { + "type": "object", + "properties": {"value": {"type": "integer"}}, + "required": ["value"], + "description": "A payload", + } + + +def test_build_model_schema_class_var_via_instance(): + instance = ModelWithClassVar(value=1) + + schema = KernelJsonSchemaBuilder.build(instance) + + assert schema == {"type": "object", "properties": {"value": {"type": "integer"}}, "required": ["value"]} + + +def test_build_model_schema_allows_serialized_payload(): + schema = KernelJsonSchemaBuilder.build(ModelWithClassVar) + + serialized = ModelWithClassVar(value=1).model_dump() + for field in schema.get("required", []): + assert field in serialized From 71390565b26687c71f4bca0e98407dd905870492 Mon Sep 17 00:00:00 2001 From: Pushpak <200882761+Pushpak731@users.noreply.github.com> Date: Sat, 3 Oct 2026 00:48:12 +0530 Subject: [PATCH 2/2] Re-trigger CI after CLA agreement