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.
This commit is contained in:
eeshsaxena 2026-07-22 08:38:14 +05:30
parent ef2062cad3
commit 868608ed4b
2 changed files with 76 additions and 0 deletions

View file

@ -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] = {

View file

@ -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"]}},
}
}
}