Try other branches in smart union in case of omit errors#12758
Try other branches in smart union in case of omit errors#12758davidhewitt merged 24 commits intopydantic:mainfrom
Conversation
|
please review :) |
davidhewitt
left a comment
There was a problem hiding this comment.
Thanks, I think it's reasonable to intercept Omit errors in this way, however we probably need to ensure that if no validator succeeds we will re-raise the Omit error.
Thanks for review! Yes, I think the behaviour is correct but its not following the If we did want to re-raise the omit error - do you think its better to add a |
davidhewitt
left a comment
There was a problem hiding this comment.
Thanks, I think we're getting there. As this is adjusting behaviour (arguably for the better), I just want to be extra sure that we have enough tests to be comfortable we're not missing any corner cases.
Co-authored-by: David Hewitt <[email protected]>
|
@davidhewitt looking at the list it seems most prudent to just raise the Omit error if there's an omit. An omit in the union is currently taking precedence over a raised error - I think this is the best option and I'm not sure there's a way to do both? |
davidhewitt
left a comment
There was a problem hiding this comment.
Agreed, I think propagating an omit error seems most correct given that is the existing behaviour.
So the PR modification is that in case of one branch raising an omit error, we now allow the other branches to try to succeed. If none of the branches succeed, we still raise the omit error as we would have previously.
I think this is a sensible improvement, and we explicitly allow ourselves to improve the smart union logic:
We reserve the right to tweak the internal smart matching algorithm in future versions of Pydantic. If you rely on very specific matching behavior, it's recommended to use union_mode='left_to_right' or discriminated unions.
| let old_fields_set_count = state.fields_set_count; | ||
|
|
||
| let mut errors = MaybeErrors::new(self.custom_error.as_ref()); | ||
| let mut omits = MaybeOmits::new(); |
There was a problem hiding this comment.
I think we can just keep a boolean marker, looks like the labels are never used:
| let mut omits = MaybeOmits::new(); | |
| // propagate an omit error if all branches failed | |
| let mut should_omit = true; |
There was a problem hiding this comment.
Would it be possible to have pydantic tests instead? That is, have a proper TypedDict defined, and use TypeAdapter to make the assertions?
In this case, it would look like:
class TD(TypedDict):
x: OnErrorOmit[int] | OnErrorOmit[str]I'm also wondering if this should be valid at all. If you simply do:
class TD(TypedDict):
x: OnErrorOmit[int | bool] # or even just OnErrorOmit[int]We get a schema build error:
TypeAdapter(TD)
"""
pydantic_core._pydantic_core.SchemaError: Error building "typed-dict" validator:
SchemaError: Field 'x': 'on_error = omit' cannot be set for required fields
"""And doing this works fine:
class TD(TypedDict, total=False):
x: OnErrorOmit[int | bool]
ta = TypeAdapter(TD)
ta.validate_python({'x': '123'})
#> {'x': 123}So presumably we're not catching a schema build error when doing OnErrorOmit[int] | OnErrorOmit[str], which leads to inconsistent validation results?
There was a problem hiding this comment.
(these tests would go in tests/types/test_union.py )
There was a problem hiding this comment.
I'm also wondering if this should be valid at all.
To be fair, this is a great question. We could potentially just start rejecting this at build time (or at least a deprecation warning that we'll reject it in v3).
There was a problem hiding this comment.
I think
list[OnErrorOmit[int] | OnErrorOmit[bool]]should be valid even if
list[OnErrorOmit[int | bool]]Produces the same result and is arguably clearer.
There was a problem hiding this comment.
What about
list[OnErrorOmit[int] | bool]... I guess that should almost certainly be invalid?
There was a problem hiding this comment.
I included test_smart_union_omit_with_defaults to document how that's treated in this PR.
pydantic.TypeAdapter(list[pydantic.OnErrorOmit[int] | bool]).validate_python(["1","True"])
> [1]Is current behavior in 2.12 which I think would be unexpected by users. I can add a schema build warning at least?
There was a problem hiding this comment.
@davidhewitt I added a warning on constructing a union schema including OnErrorOmit :)
There was a problem hiding this comment.
I'm -0 on having this warning, as it doesn't take into account all cases (e.g. what if you do: type A = OnErrorOmit[int]; U = A | OnErrorOmit[str]).
Ideally we would simplify such unions when building the validator (to end up with a single one), which would give us the ability to catch the error for typed dicts. But this is not trivial.
So I think let's remove the warnings logic, move the tests to pydantic (https://github.com/pydantic/pydantic/pull/12758/changes#r2777402600), and it should be good to merge.
…MaybeOmits struct
Change Summary
fix #12750 by updating the
UnionValidator.validate_smartinunion.rsto handleValError::Omiterrors by continuing to try other degrees of exactness instead of propagating the omit error and causing the value to be omitted.Added a new test,
test_td_smart_union_omit, intest_union.pyto verify that the union validator properly supports theomitbehavior when multiple union branches are present, ensuring that omitted errors do not prevent successful validation. This test fails on primary:Related issue number
fix #12750
Checklist
Selected Reviewer: @Viicos