fix: strip empty {} entries from anyOf/oneOf in strict JSON schema#5137
fix: strip empty {} entries from anyOf/oneOf in strict JSON schema#5137theomonnom merged 1 commit intomainfrom
Conversation
…5130) When a Pydantic model field is typed as Union[Literal[...], Any] (common in codegen clients like Fern for forward-compatible 'open enums'), model_json_schema() produces an anyOf containing a bare {} entry. OpenAI's strict mode rejects schemas containing {} because it lacks a type field. Strip empty schema objects from anyOf and oneOf arrays before recursing. When stripping leaves a single variant, unwrap it (analogous to existing allOf handling). Removing {} is semantically neutral since it is the JSON Schema identity element for anyOf (matches anything). Co-Authored-By: guoyangzhen <upgyz@qq.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| json_schema.update( | ||
| _ensure_strict_json_schema(variants[0], path=(*path, union_key, "0"), root=root) | ||
| ) | ||
| json_schema.pop(union_key, None) |
There was a problem hiding this comment.
🔴 Single-variant anyOf/oneOf unwrapping loses inner union when it has the same key
When stripping {} entries from anyOf/oneOf leaves exactly one variant, the code at livekit-agents/livekit/agents/llm/_strict.py:87-90 calls json_schema.update(processed_variant) then json_schema.pop(union_key, None). If the processed variant itself contains the same union key (e.g., an inner anyOf inside an outer anyOf), the update overwrites json_schema[union_key] with the inner union's variants, and then pop unconditionally removes it — silently dropping all type information.
Reproduction and expected behavior
Input schema: {"anyOf": [{"anyOf": [{"type": "string"}, {"type": "integer"}]}, {}]}
Expected output: {"anyOf": [{"type": "string"}, {"type": "integer"}]}
Actual output: {} — all type information is lost.
The fix is to swap the order: call pop before update, so the outer union key is removed first, and then the inner union (if present) is correctly merged in.
| json_schema.update( | |
| _ensure_strict_json_schema(variants[0], path=(*path, union_key, "0"), root=root) | |
| ) | |
| json_schema.pop(union_key, None) | |
| json_schema.pop(union_key, None) | |
| json_schema.update( | |
| _ensure_strict_json_schema(variants[0], path=(*path, union_key, "0"), root=root) | |
| ) |
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary
{}schema objects fromanyOf/oneOfarrays in_ensure_strict_json_schemaanyOf/oneOfafter stripping (analogous to existingallOfhandling)Fixes #5130
Supersedes #5134
Context
When a Pydantic model field is typed as
Union[Literal[...], Any](common in codegen clients like Fern for forward-compatible "open enums"),model_json_schema()produces ananyOfcontaining a bare{}entry. OpenAI's strict mode rejects schemas containing{}because it lacks a type field.{}is the JSON Schema identity element foranyOf(matches anything), so removing it is semantically neutral.Changes
livekit-agents/livekit/agents/llm/_strict.py: UnifiedanyOf/oneOfhandling into a single loop that filters{}entries and unwraps single-variant unionstests/test_tools.py: AddedTestEmptySchemaStrippingwith tests for open enum models, nested models, and single-variant unwrappingTest plan
uv run pytest tests/test_tools.py— all 27 tests passruff checkandruff formatpassBased on work by @guoyangzhen in #5134.
🤖 Generated with Claude Code