Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
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
  • Loading branch information
Pushpak731 committed Oct 2, 2026
commit 4007fd3509342c18a8c459975652bad31585e3dd
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down
103 changes: 102 additions & 1 deletion python/tests/unit/schema/test_schema_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Loading