From 868608ed4b147b3c9860f36296cae0872c163608 Mon Sep 17 00:00:00 2001 From: eeshsaxena Date: Wed, 22 Jul 2026 08:38:14 +0530 Subject: [PATCH] fix(core): normalize uppercase types inside a JSON Schema type list normalize_json_schema_types only lowercased `type` when it was a string, so a list of types - the standard way to mark a field nullable, e.g. ["STRING", "NULL"] - fell through to the generic list recursion, which returns bare strings untouched. Providers that emit uppercase types therefore kept them on every nullable field while sibling keys were normalized correctly: normalize_tool_schema({"function": {"parameters": { "type": "OBJECT", "properties": {"x": {"type": ["STRING", "NULL"]}}}}}) # -> parameters.type == "object" but x.type == ["STRING", "NULL"] Normalize each entry of a type list, leaving unrecognised entries alone. Adds tests for this module, which had none. --- .../json_validation_rule.py | 9 +++ .../test_json_validation_rule.py | 67 +++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 tests/test_litellm/litellm_core_utils/test_json_validation_rule.py diff --git a/litellm/litellm_core_utils/json_validation_rule.py b/litellm/litellm_core_utils/json_validation_rule.py index c73b62f8a21..98901c433a3 100644 --- a/litellm/litellm_core_utils/json_validation_rule.py +++ b/litellm/litellm_core_utils/json_validation_rule.py @@ -52,6 +52,15 @@ def normalize_json_schema_types( for key, value in schema.items(): if key == "type" and isinstance(value, str) and value in type_mapping: normalized_schema[key] = type_mapping[value] + elif key == "type" and isinstance(value, list): + # JSON Schema also allows a list of types, which is the usual way + # to mark a field nullable (e.g. ["STRING", "NULL"]). Without this + # branch those entries fall through to the generic list recursion, + # which leaves the bare strings uppercase. + normalized_schema[key] = [ + type_mapping.get(entry, entry) if isinstance(entry, str) else entry + for entry in value + ] elif key == "properties" and isinstance(value, dict): # Recursively normalize properties normalized_schema[key] = { diff --git a/tests/test_litellm/litellm_core_utils/test_json_validation_rule.py b/tests/test_litellm/litellm_core_utils/test_json_validation_rule.py new file mode 100644 index 00000000000..5ff6bee986e --- /dev/null +++ b/tests/test_litellm/litellm_core_utils/test_json_validation_rule.py @@ -0,0 +1,67 @@ +from litellm.litellm_core_utils.json_validation_rule import ( + normalize_json_schema_types, + normalize_tool_schema, +) + + +def test_normalizes_a_plain_string_type(): + assert normalize_json_schema_types({"type": "STRING"}) == {"type": "string"} + + +def test_normalizes_a_list_of_types(): + """A list of types is how a nullable field is expressed. + + Regression: these entries fell through to the generic list recursion, which + returns bare strings untouched, so they stayed uppercase. + """ + assert normalize_json_schema_types({"type": ["STRING", "NULL"]}) == { + "type": ["string", "null"] + } + + +def test_normalizes_a_list_of_types_when_nested(): + schema = {"properties": {"a": {"type": ["INTEGER", "NULL"]}}} + + assert normalize_json_schema_types(schema) == { + "properties": {"a": {"type": ["integer", "null"]}} + } + + +def test_leaves_unknown_type_entries_alone(): + assert normalize_json_schema_types({"type": ["STRING", "custom"]}) == { + "type": ["string", "custom"] + } + + +def test_still_normalizes_properties_items_and_anyof(): + schema = { + "type": "OBJECT", + "properties": {"xs": {"type": "ARRAY", "items": {"type": "INTEGER"}}}, + "anyOf": [{"type": "STRING"}], + } + + assert normalize_json_schema_types(schema) == { + "type": "object", + "properties": {"xs": {"type": "array", "items": {"type": "integer"}}}, + "anyOf": [{"type": "string"}], + } + + +def test_tool_schema_normalizes_a_nullable_parameter(): + tool = { + "function": { + "parameters": { + "type": "OBJECT", + "properties": {"x": {"type": ["STRING", "NULL"]}}, + } + } + } + + assert normalize_tool_schema(tool) == { + "function": { + "parameters": { + "type": "object", + "properties": {"x": {"type": ["string", "null"]}}, + } + } + }