From f72cfb4ea21241563f435a9569fbc6ec1b93d87d Mon Sep 17 00:00:00 2001 From: VANDRANKI Date: Wed, 30 Sep 2026 12:40:54 -0400 Subject: [PATCH] Python: add typing.Literal support to KernelJsonSchemaBuilder --- .../schema/kernel_json_schema_builder.py | 15 +++++- .../tests/unit/schema/test_schema_builder.py | 46 ++++++++++++++++++- 2 files changed, 58 insertions(+), 3 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..aaaeed0f390d 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, Literal, 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 @@ -196,12 +196,23 @@ def handle_complex_type( if structured_output: schema["additionalProperties"] = False return schema + if origin is Literal: + values = [arg.value if isinstance(arg, Enum) else arg for arg in args] + value_types = {"null" if v is None else TYPE_MAPPING.get(type(v), "string") for v in values} + ordered_types = sorted(value_types, key=lambda t: (t == "null", t)) + schema = {"type": ordered_types[0] if len(ordered_types) == 1 else ordered_types, "enum": values} + if description: + schema["description"] = description + return schema if origin in {Union, types.UnionType}: # Handle Optional[T] (Union[T, None]) by making schema nullable if len(args) == 2 and type(None) in args: non_none_type = args[0] if args[1] is type(None) else args[1] schema = cls.build(non_none_type, structured_output=structured_output) - schema["type"] = [schema["type"], "null"] + schema_types = schema["type"] if isinstance(schema["type"], list) else [schema["type"]] + schema["type"] = schema_types if "null" in schema_types else [*schema_types, "null"] + if "enum" in schema and None not in schema["enum"]: + schema["enum"] = [*schema["enum"], None] if description: schema["description"] = description if structured_output: diff --git a/python/tests/unit/schema/test_schema_builder.py b/python/tests/unit/schema/test_schema_builder.py index 5d24a599c96c..22e725ac2045 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, Literal, Optional, Union from unittest.mock import Mock import pytest @@ -455,3 +455,47 @@ def test_build_schema_with_nonpydantic_structured_output(): } assert structured_output_schema == expected_schema + + +class LiteralModel(KernelBaseModel): + level: Literal["low", "high"] + maybe: Literal["a", "b"] | None = None + + +@pytest.mark.parametrize( + ("literal_type", "expected"), + [ + (Literal["fast", "slow"], {"type": "string", "enum": ["fast", "slow"]}), + (Literal[1, 2], {"type": "integer", "enum": [1, 2]}), + (Literal[True], {"type": "boolean", "enum": [True]}), + (Literal["a", 1], {"type": ["integer", "string"], "enum": ["a", 1]}), + (Optional[Literal["fast", "slow"]], {"type": ["string", "null"], "enum": ["fast", "slow", None]}), # noqa: UP045 + (Literal["fast", "slow"] | None, {"type": ["string", "null"], "enum": ["fast", "slow", None]}), + (Optional[Literal["a", None]], {"type": ["string", "null"], "enum": ["a", None]}), # noqa: UP045 + (list[Literal["a", "b"]], {"type": "array", "items": {"type": "string", "enum": ["a", "b"]}}), + ], +) +def test_build_literal(literal_type, expected): + assert KernelJsonSchemaBuilder.build(literal_type) == expected + + +def test_build_literal_with_description(): + schema = KernelJsonSchemaBuilder.build(Literal["fast", "slow"], "speed mode") + assert schema == {"type": "string", "enum": ["fast", "slow"], "description": "speed mode"} + + +def test_build_literal_enum_member(): + schema = KernelJsonSchemaBuilder.build(Literal[EnumTest.OPTION_A]) + assert schema == {"type": "string", "enum": ["OptionA"]} + + +def test_build_model_with_literal_fields(): + schema = KernelJsonSchemaBuilder.build(LiteralModel) + assert schema == { + "type": "object", + "properties": { + "level": {"type": "string", "enum": ["low", "high"]}, + "maybe": {"type": ["string", "null"], "enum": ["a", "b", None]}, + }, + "required": ["level"], + }