fix(schema): preserve dependentRequired property names

This commit is contained in:
Atakan 2026-07-27 23:53:26 +03:00 committed by Teknium
parent 3cedac00b7
commit 9dcb44a219
2 changed files with 73 additions and 1 deletions

View file

@ -867,3 +867,73 @@ def test_unrename_passes_unknown_keys_through():
def test_sanitize_property_key_empty_falls_back():
assert sanitize_property_key("~~~") == "___"
assert sanitize_property_key("") == "param"
# ---------------------------------------------------------------------------
# dependentRequired -- literal property-name strings must survive
# ---------------------------------------------------------------------------
def test_dependent_required_preserved_through_public_api():
"""dependentRequired values are literal property names, not schemas."""
schema = {
"type": "object",
"properties": {
"owner": {"type": "string"},
"repo": {"type": "string"},
"organization": {"type": "string"},
},
"dependentRequired": {
"owner": ["repo", "organization"],
"repo": ["owner"],
},
}
tools = [_tool("t", copy.deepcopy(schema))]
out = sanitize_tool_schemas(tools)
params = out[0]["function"]["parameters"]
dep = params.get("dependentRequired", {})
# Values are the original property-name strings unchanged.
assert dep.get("owner") == ["repo", "organization"]
assert dep.get("repo") == ["owner"]
# Normal property schemas are still present and valid.
assert params["properties"]["owner"] == {"type": "string"}
assert params["properties"]["repo"] == {"type": "string"}
assert params["properties"]["organization"] == {"type": "string"}
def test_dependent_required_does_not_mutate_original_input():
"""The original schema's dependentRequired must be unchanged after sanitize."""
original_dep = {"owner": ["repo", "organization"], "repo": ["owner"]}
schema = {
"type": "object",
"properties": {
"owner": {"type": "string"},
"repo": {"type": "string"},
"organization": {"type": "string"},
},
"dependentRequired": {k: list(v) for k, v in original_dep.items()},
}
saved_copy = copy.deepcopy(schema)
tools = [_tool("t", schema)]
_ = sanitize_tool_schemas(tools)
assert schema == saved_copy
assert schema["dependentRequired"] == original_dep
def test_dependent_schemas_still_recursively_sanitized():
"""dependentSchemas (real schemas, not literal lists) must still be sanitized."""
schema = {
"type": "object",
"properties": {
"owner": {"type": "string"},
},
"dependentSchemas": {
"owner": {"type": "object"}, # bare object -- needs properties: {}
},
}
tools = [_tool("t", copy.deepcopy(schema))]
out = sanitize_tool_schemas(tools)
dep_schemas = out[0]["function"]["parameters"]["dependentSchemas"]
assert dep_schemas["owner"] == {"type": "object", "properties": {}}, (
f"dependentSchemas['owner'] was not fully sanitized: {dep_schemas['owner']!r}"
)

View file

@ -406,11 +406,13 @@ def _sanitize_node(node: Any, path: str) -> Any:
_sanitize_node(item, f"{path}.{key}[{i}]")
for i, item in enumerate(value)
]
elif key in {"required", "enum", "examples"}:
elif key in {"required", "enum", "examples", "dependentRequired"}:
# Schema "sibling" keywords whose values are NOT schemas:
# - ``required``: list of property-name strings
# - ``enum``: list of literal values (any JSON type)
# - ``examples``: list of example values (any JSON type)
# - ``dependentRequired``: mapping of property names to lists of
# required property-name strings (JSON Schema 2020-12)
# Recursing into these with _sanitize_node() would mis-interpret
# literal strings like "path" as bare-string schemas and replace
# them with {"type": "object"} dicts. Pass through unchanged