Skip to content

fix: strip empty {} entries from anyOf/oneOf in strict JSON schema#5137

Merged
theomonnom merged 1 commit intomainfrom
fix/strict-schema-empty-anyof
Mar 18, 2026
Merged

fix: strip empty {} entries from anyOf/oneOf in strict JSON schema#5137
theomonnom merged 1 commit intomainfrom
fix/strict-schema-empty-anyof

Conversation

@theomonnom
Copy link
Member

Summary

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 an anyOf containing a bare {} entry. OpenAI's strict mode rejects schemas containing {} because it lacks a type field.

{} is the JSON Schema identity element for anyOf (matches anything), so removing it is semantically neutral.

Changes

  • livekit-agents/livekit/agents/llm/_strict.py: Unified anyOf/oneOf handling into a single loop that filters {} entries and unwraps single-variant unions
  • tests/test_tools.py: Added TestEmptySchemaStripping with tests for open enum models, nested models, and single-variant unwrapping

Test plan

  • uv run pytest tests/test_tools.py — all 27 tests pass
  • ruff check and ruff format pass
  • CI green

Based on work by @guoyangzhen in #5134.

🤖 Generated with Claude Code

…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>
@chenghao-mou chenghao-mou requested a review from a team March 18, 2026 05:49
@theomonnom theomonnom merged commit 477f820 into main Mar 18, 2026
18 of 20 checks passed
@theomonnom theomonnom deleted the fix/strict-schema-empty-anyof branch March 18, 2026 05:53
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View 3 additional findings in Devin Review.

Open in Devin Review

Comment on lines +87 to +90
json_schema.update(
_ensure_strict_json_schema(variants[0], path=(*path, union_key, "0"), root=root)
)
json_schema.pop(union_key, None)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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.

Suggested change
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)
)
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

_ensure_strict_json_schema should handle empty {} entries in anyOf/oneOf

1 participant