fix(types): map anyOf/oneOf to a Union instead of dropping to Any - #1973
fix(types): map anyOf/oneOf to a Union instead of dropping to Any#1973ErenAta16 wants to merge 1 commit into
Conversation
schema_type_to_python handled enum, const, type and JSON Schema type arrays, but not anyOf or oneOf, so a property spelled with either union keyword fell through to Any and carried no constraint at all. Pydantic emits anyOf for Optional[T] and Union[...], so a model round tripped through json_schema_dict_to_pydantic lost those fields entirely while the equivalent type-array spelling kept them. Map each branch recursively and combine with Union, matching what the type-array path already does. oneOf maps to the same Union: typing cannot express exclusivity, but the set of allowed types is strictly narrower than Any. Closes dottxt-ai#1950.
|
📚 Documentation preview: https://dottxt-ai.github.io/outlines/pr-preview/pr-1973/ Preview updates automatically with each commit. |
|
Closing this in favour of #1951, which does the same thing and was open nine days before I opened this. I should have found it — that's a preflight failure on my part, and the second one today. Two differences worth recording for #1951 rather than losing here.
The extra tests here — both keywords parametrised, the |
Closes #1950.
What
schema_type_to_pythondispatches onenum,const,typeand JSON Schema type arrays, but not onanyOf/oneOf. A property spelled with either union keyword falls through every branch and returnsAny, so the field ends up with no constraint.That matters more than a hand-written schema suggests, because Pydantic emits
anyOfforOptional[T]andUnion[...]. A model round-tripped throughjson_schema_dict_to_pydanticloses exactly those fields:maybe_intAnyOptional[int]eitherAnyUnion[int, str]plainintintand the regenerated schema goes from
{"title": "Maybe Int"}back to{"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Maybe Int"}— identical to what Pydantic produced, so the round trip is lossless for these fields now.The same constraint written as a type array already worked, which is what made the gap easy to miss:
How
Handle both keywords before the
typedispatch, mapping each branch throughschema_type_to_pythonrecursively and combining withUnion— the same shape the existing type-array branch uses.oneOfmaps to the sameUnion. Python typing can't express exclusivity, so an input matching two branches will validate where the schema says it shouldn't; that's a widening, but a far smaller one thanAny, which accepts values no branch allows. Called out in a comment so the trade-off isn't silently inherited.Tests
Five tests: both keywords to
Union, both with anullbranch toOptional, the two spellings agreeing, recursive branch mapping (anyOfofarray/stringtoUnion[List[int], str]), and the Pydantic round trip above.They fail 7/30 on
mainand pass 30/30 with the change.tests/types/test_dsl.pyis unchanged at 52 passed.ruff checkis clean.tests/types/test_custom_types.pyfails to import in my environment becauseairportsdataisn't installed; that's unrelated and identical on a clean checkout.Note
schema_type_to_pythonis also touched by #1970 (theadditionalPropertiesbranch insideelif t == "object"). These are different branches of the same function, so they'll conflict textually but not semantically if both land.