Skip to content

Try other branches in smart union in case of omit errors#12758

Merged
davidhewitt merged 24 commits intopydantic:mainfrom
mikeedjones:bugfix/pydantic-core/omit-errors-in-unions
Mar 18, 2026
Merged

Try other branches in smart union in case of omit errors#12758
davidhewitt merged 24 commits intopydantic:mainfrom
mikeedjones:bugfix/pydantic-core/omit-errors-in-unions

Conversation

@mikeedjones
Copy link
Copy Markdown
Contributor

@mikeedjones mikeedjones commented Jan 30, 2026

Change Summary

fix #12750 by updating the UnionValidator.validate_smart in union.rs to handle ValError::Omit errors 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, in test_union.py to verify that the union validator properly supports the omit behavior when multiple union branches are present, ensuring that omitted errors do not prevent successful validation. This test fails on primary:

__________ test_td_smart_union_omit __________

    def test_td_smart_union_omit() -> None:
        validator = SchemaValidator(
            core_schema.typed_dict_schema(
                fields={
                    'x': core_schema.typed_dict_field(
                        core_schema.union_schema(
                            [
                                core_schema.with_default_schema(core_schema.int_schema(), on_error='omit'),
                                core_schema.with_default_schema(core_schema.bool_schema(), on_error='omit'),
                            ]
                        )
                    )
                },
            )
        )
>       assert validator.validate_python({'x': '123'}) == {'x': 123}
E       AssertionError: assert {} == {'x': 123}
E         
E         Right contains 1 more item:
E         {'x': 123}
E         Use -v to get more diff

Related issue number

fix #12750

Checklist

  • The pull request title is a good summary of the changes - it will be used in the changelog
  • Unit tests for the changes exist
  • Tests pass on CI
  • Documentation reflects the changes where applicable
  • My PR is ready to review, please add a comment including the phrase "please review" to assign reviewers

Selected Reviewer: @Viicos

@github-actions github-actions Bot added the relnotes-fix Used for bugfixes. label Jan 30, 2026
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Jan 30, 2026

Merging this PR will not alter performance

✅ 212 untouched benchmarks


Comparing mikeedjones:bugfix/pydantic-core/omit-errors-in-unions (ff87040) with main (4f818e0)

Open in CodSpeed

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jan 30, 2026

Coverage report

This PR does not seem to contain any modification to coverable code.

@mikeedjones
Copy link
Copy Markdown
Contributor Author

mikeedjones commented Jan 30, 2026

please review :)

Copy link
Copy Markdown
Collaborator

@davidhewitt davidhewitt left a comment

Choose a reason for hiding this comment

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

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.

Comment thread pydantic-core/tests/validators/test_union.py Outdated
@mikeedjones
Copy link
Copy Markdown
Contributor Author

mikeedjones commented Feb 2, 2026

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 ValError::Omit code path when that error has been raised (potentially with the legitimate user-set exactness). I'm not sure it's worth adding the overhead to propagate "legitimate" Omit Errors now - what do you think? Have added that test and expanded the comment in union.rs.

If we did want to re-raise the omit error - do you think its better to add a MaybeOmits and associated interface or extend MaybeErorrs? Leaning towards the former.

Copy link
Copy Markdown
Collaborator

@davidhewitt davidhewitt left a comment

Choose a reason for hiding this comment

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

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.

Comment thread pydantic-core/tests/validators/test_union.py Outdated
@mikeedjones
Copy link
Copy Markdown
Contributor Author

@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 davidhewitt changed the title fix: continue checking smart union on omit error Try other branches in smart union in case of omit errors Feb 7, 2026
Copy link
Copy Markdown
Collaborator

@davidhewitt davidhewitt left a comment

Choose a reason for hiding this comment

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

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.

Comment thread pydantic-core/src/validators/union.rs Outdated
let old_fields_set_count = state.fields_set_count;

let mut errors = MaybeErrors::new(self.custom_error.as_ref());
let mut omits = MaybeOmits::new();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think we can just keep a boolean marker, looks like the labels are never used:

Suggested change
let mut omits = MaybeOmits::new();
// propagate an omit error if all branches failed
let mut should_omit = true;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

(these tests would go in tests/types/test_union.py )

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think

list[OnErrorOmit[int] | OnErrorOmit[bool]]

should be valid even if

list[OnErrorOmit[int | bool]]

Produces the same result and is arguably clearer.

Copy link
Copy Markdown
Collaborator

@davidhewitt davidhewitt Feb 11, 2026

Choose a reason for hiding this comment

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

What about

list[OnErrorOmit[int] | bool]

... I guess that should almost certainly be invalid?

Copy link
Copy Markdown
Contributor Author

@mikeedjones mikeedjones Feb 11, 2026

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Contributor Author

@mikeedjones mikeedjones Feb 12, 2026

Choose a reason for hiding this comment

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

@davidhewitt I added a warning on constructing a union schema including OnErrorOmit :)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

@mikeedjones mikeedjones Mar 12, 2026

Choose a reason for hiding this comment

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

Thank you @Viicos ! I added pydantic side tests to tests/types/test_union.py in ccd07fa but have now removed the pydantic-core side tests and warning logic. Is there anything else this needs?

@Viicos Viicos added awaiting author revision awaiting changes from the PR author and removed ready for review labels Mar 5, 2026
Copy link
Copy Markdown
Collaborator

@davidhewitt davidhewitt left a comment

Choose a reason for hiding this comment

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

Thanks!

@davidhewitt davidhewitt merged commit 6178953 into pydantic:main Mar 18, 2026
81 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting author revision awaiting changes from the PR author relnotes-fix Used for bugfixes.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Setting 'on_error': 'omit' on typed-dict values omits valid strings when using smart union mode.

3 participants