Skip to content

Count validated model fields once in smart unions - #13731

Merged
Viicos merged 3 commits into
pydantic:mainfrom
tamird:tamird/fix-smart-union-field-count
Aug 28, 2026
Merged

Viicos merged 3 commits into
pydantic:mainfrom
tamird:tamird/fix-smart-union-field-count

Conversation

@tamird

@tamird tamird commented Aug 28, 2026 •

Copy link
Copy Markdown
Contributor

Change Summary

A model before validator converts JSON input to Python values. The Python-input helper counted supplied fields, then the outer validator counted them again. This let a smaller union member with a before validator outrank a larger member using the plain JSON path.

Count successfully validated, explicitly supplied known fields once in each input-specific helper. Preserve nested counts without counting extra keys or defaults as local fields.

The regression covers Python and JSON input, both union orders, and ignored or allowed extras.

Edit by @Viicos: this also does not count extra fields anymore, this was most likely a regression too and doesn't really make sense.

Related issue number

Fixes #13729.

Checklist

  • The pull request title is a good summary of the changes
  • Unit tests for the changes exist
  • Tests pass on CI
  • Documentation reflects the changes where applicable (the documented smart-union ranking is unchanged)
  • My PR is ready to review

Prepared with AI assistance.

A model before validator converts JSON input to Python values.
ModelFieldsValidator counted supplied fields in validate_by_get_item and
again in validate, so a smaller union member with a before validator
could outrank a larger member using the plain JSON path.

Keep counting in the input-specific helpers, excluding extra keys and
defaults. Cover Python and JSON inputs, both union orders, and allowed
or ignored extras.

Fixes pydantic#13729
@github-actions

Copy link
Copy Markdown
Contributor

@tamird You do not have permission to open a PR without being assigned to the referenced issue (#13729).

@github-actions github-actions Bot closed this Aug 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Coverage report

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

@codspeed

codspeed Bot commented Aug 28, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 216 untouched benchmarks
⏩ 163 skipped benchmarks1


Comparing tamird:tamird/fix-smart-union-field-count (e6bc0fe) with main (965c23d)

Open in CodSpeed

Footnotes

  1. 163 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@Kludex Kludex reopened this Aug 28, 2026
Add tests for smart union fields and extra handling.

@Viicos Viicos left a comment

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.

Thanks @tamird

@Viicos
Viicos merged commit 1b108de into pydantic:main Aug 28, 2026
89 checks passed
Viicos added a commit that referenced this pull request Aug 28, 2026
@tamird
tamird deleted the tamird/fix-smart-union-field-count branch August 28, 2026 09:55
736-c41-2c1-e464fc974 added a commit to Swiss-Armed-Forces/Loom that referenced this pull request Sep 15, 2026
This MR contains the following updates:

| Package | Type | Update | Change | OpenSSF |
|---|---|---|---|---|
| [numpy](https://github.com/numpy/numpy) ([changelog](https://numpy.org/doc/stable/release)) | dependencies | patch | `2.5.2` → `2.5.3` | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/numpy/numpy/badge)](https://securityscorecards.dev/viewer/?uri=github.com/numpy/numpy) |
| [openai](https://github.com/openai/openai-python) | dependencies | patch | `2.52.0` → `2.52.1` | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/openai/openai-python/badge)](https://securityscorecards.dev/viewer/?uri=github.com/openai/openai-python) |
| [pydantic](https://github.com/pydantic/pydantic) ([changelog](https://docs.pydantic.dev/latest/changelog/)) | dependencies | patch | `2.13.4` → `2.13.5` | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/pydantic/pydantic/badge)](https://securityscorecards.dev/viewer/?uri=github.com/pydantic/pydantic) |
| [pydantic-ai](https://github.com/pydantic/pydantic-ai) ([changelog](https://github.com/pydantic/pydantic-ai/releases)) | dependencies | patch | `2.27.0` → `2.27.1` | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/pydantic/pydantic-ai/badge)](https://securityscorecards.dev/viewer/?uri=github.com/pydantic/pydantic-ai) |
| [types-requests](https://github.com/python/typeshed) ([changelog](https://github.com/typeshed-internal/stub_uploader/blob/main/data/changelogs/requests.md)) | dependencies | patch | `2.33.0.20260712` → `2.33.0.20260906` | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/python/typeshed/badge)](https://securityscorecards.dev/viewer/?uri=github.com/python/typeshed) |
| [uvicorn](https://github.com/Kludex/uvicorn) ([changelog](https://uvicorn.dev/release-notes)) | dependencies | patch | `0.52.3` → `0.52.4` | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/Kludex/uvicorn/badge)](https://securityscorecards.dev/viewer/?uri=github.com/Kludex/uvicorn) |

---

### Release Notes

<details>
<summary>numpy/numpy (numpy)</summary>

### [`v2.5.3`](https://github.com/numpy/numpy/releases/tag/v2.5.3): (Sep 6, 2026)

[Compare Source](numpy/numpy@v2.5.2...v2.5.3)

### NumPy 2.5.3 Release Notes

The NumPy 2.5.3 is a patch release that fixes bugs discovered after the 2.5.2
release. Apart from the usual bug and maintenance work, there are a number of
StringDType related fixes for problems discovered during the ongoing string
work in the main branch.

This release supports Python versions 3.12-3.15

#### Changes

- Casting a fixed-width byte string array (`np.bytes_`) to `StringDType`
  now raises `TypeError` when the bytes are not valid UTF-8. Previously the
  invalid bytes were stored as-is and later caused undefined behavior in
  string operations.

  ([gh-32296](numpy/numpy#32296))

- `MaskedArray._fill_value` would become stale when ufuncs that change dtype
  left the result holding a fill\_value typed for the old dtype. The mismatch
  was silent until something later called `_check_fill_value`, such as
  `.view()`, and then a `TypeError` would be raised. Now, when the copied
  fill\_value is no longer valid for the new dtype, fall back to the
  default fill\_value for that dtype instead of propagating the stale value.
  This may raise a `ComplexWarning` if the fill\_value is complex and the
  new dtype is real.

  ([gh-32423](numpy/numpy#32423))

#### Contributors

A total of 9 people contributed to this release. People with a "+" by their
names contributed a patch for the first time.

- Charles Harris
- Iason Krommydas
- James Davies +
- Joren Hammudoglu
- Maanas Arora
- Matti Picus
- Nathan Goldbaum
- Shikhar Goel +
- Yeonho Kim +

#### Pull requests merged

A total of 27 pull requests were merged for this release.

- [#&#8203;32235](numpy/numpy#32235): MAINT: Prepare 2.5.x for further development
- [#&#8203;32289](numpy/numpy#32289): BUG: raise ValueError when reading into record array with references...
- [#&#8203;32290](numpy/numpy#32290): BUG: avoid uninitialized memory access / NULL-pointer deref in...
- [#&#8203;32291](numpy/numpy#32291): TYP: fix `np.random.{get,set}_bit_generator` implicit re-exports...
- [#&#8203;32292](numpy/numpy#32292): BUG: don't assume strides are a multiple of itemsize in stringdtype...
- [#&#8203;32293](numpy/numpy#32293): CI: fix ccache CC override, add CXX in mac Conda CI ([#&#8203;32285](numpy/numpy#32285))
- [#&#8203;32303](numpy/numpy#32303): BUG: Fix ref leak in \[convert\_from\_type]{#convert\_from\_type} for custom scalar types...
- [#&#8203;32304](numpy/numpy#32304): BUG: fix a number of issues around iterators and StringDType...
- [#&#8203;32326](numpy/numpy#32326): TST: avoid allocating huge tuple of arrays in concatenate test...
- [#&#8203;32338](numpy/numpy#32338): BUG: avoid possible UB in 'safe' multiplication helpers ([#&#8203;32294](numpy/numpy#32294))
- [#&#8203;32339](numpy/numpy#32339): MAINT: use `PyObject_` functions instead of raw `PyArray_ ones` ([#&#8203;32331](numpy/numpy#32331))
- [#&#8203;32378](numpy/numpy#32378): BUG: validate UTF-8 and harden StringDType bounds handling ([#&#8203;32296](numpy/numpy#32296))
- [#&#8203;32380](numpy/numpy#32380): BUG: fix two error handling mistakes in stringdtype replace loop...
- [#&#8203;32381](numpy/numpy#32381): BUG: fix visibility annotations for functions in StringDType...
- [#&#8203;32384](numpy/numpy#32384): MAINT: Update ml\_dtypes pin to 8/21/2026.
- [#&#8203;32385](numpy/numpy#32385): MAINT: Update numpy/\_core/src/umath/svml
- [#&#8203;32410](numpy/numpy#32410): MAINT: skip failing cython limited API tests on Cython 3.3.0...
- [#&#8203;32427](numpy/numpy#32427): BUG: close duplicated file descriptor if fdopen fails ([#&#8203;32386](numpy/numpy#32386))
- [#&#8203;32428](numpy/numpy#32428): MAINT: add missing space in warning and error messages ([#&#8203;32405](numpy/numpy#32405))
- [#&#8203;32430](numpy/numpy#32430): BUG: fix error handling in StringDType to fixed-width bytes case...
- [#&#8203;32441](numpy/numpy#32441): MAINT: Only run nightly BLAS tests on main.
- [#&#8203;32471](numpy/numpy#32471): BUG: fix memory leak in StringDType creation error path ([#&#8203;32470](numpy/numpy#32470))
- [#&#8203;32477](numpy/numpy#32477): BUG: fix stale fill\_value after ufuncs change MaskedArray dtype...
- [#&#8203;32478](numpy/numpy#32478): MAINT: exit deadlock tests quickly on slow hardware ([#&#8203;32466](numpy/numpy#32466))
- [#&#8203;32481](numpy/numpy#32481): BUG: Backport StringDType byteorder fixes
- [#&#8203;32506](numpy/numpy#32506): DOC: use static scipy doc site for intershpinx ([#&#8203;32503](numpy/numpy#32503))
- [#&#8203;32509](numpy/numpy#32509): BUG: fix crash in ufunc.resolve\_dtypes with a Python scalar type...

</details>

<details>
<summary>openai/openai-python (openai)</summary>

### [`v2.52.1`](https://github.com/openai/openai-python/blob/HEAD/CHANGELOG.md#2521-2026-07-31)

[Compare Source](openai/openai-python@v2.52.0...v2.52.1)

Full Changelog: [v2.52.0...v2.52.1](openai/openai-python@v2.52.0...v2.52.1)

##### Chores

- **ci:** pin setup-uv v5 to its underlying commit ([#&#8203;3560](openai/openai-python#3560)) ([cbdc98b](openai/openai-python@cbdc98b))

</details>

<details>
<summary>pydantic/pydantic (pydantic)</summary>

### [`v2.13.5`](https://github.com/pydantic/pydantic/releases/tag/v2.13.5)

[Compare Source](pydantic/pydantic@v2.13.4...v2.13.5)

#### v2.13.5 (2026-08-28)

##### What's Changed

##### Fixes

- Allow reuse of validators when plugins are set by [@&#8203;Viicos](https://github.com/Viicos) in [#&#8203;13535](pydantic/pydantic#13535)
- Fix missing GC traversal on some `pydantic-core` struct fields by [@&#8203;Viicos](https://github.com/Viicos) in [#&#8203;13624](pydantic/pydantic#13624)
- Fix missing GC traversal in `pydantic-core` for `GeneralFieldsSerializer` by [@&#8203;Viicos](https://github.com/Viicos) in [#&#8203;13629](pydantic/pydantic#13629)
- Count validated model fields once in smart unions by [@&#8203;tamird](https://github.com/tamird) in [#&#8203;13731](pydantic/pydantic#13731)

</details>

<details>
<summary>pydantic/pydantic-ai (pydantic-ai)</summary>

### [`v2.27.1`](https://github.com/pydantic/pydantic-ai/releases/tag/v2.27.1): (2026-08-10)

[Compare Source](pydantic/pydantic-ai@v2.27.0...v2.27.1)

##### 🛡️ Security

This release fixed an information-disclosure issue: retry-prompt content (validation feedback sent back to the model, which can quote invalid values from its output) was not redacted by `InstrumentationSettings(include_content=False)` when the retry was not tied to a tool call. Now disclosed as [GHSA-3gh4-cghq-f8v4](GHSA-3gh4-cghq-f8v4) (low). Fixed here in `2.27.1` ([#&#8203;7357](pydantic/pydantic-ai#7357)); v1 users should upgrade to `1.107.4` or later.

<!-- Release notes generated using configuration in .github/release.yml at main -->

#### What's Changed

##### 🐛 Bug Fixes

- Restore tool spans for failed argument validation by [@&#8203;adtyavrdhn](https://github.com/adtyavrdhn) in [#&#8203;6601](pydantic/pydantic-ai#6601)
- Fix `XaiStreamedResponse` finish\_reason mapping for streaming responses by [@&#8203;pydanty](https://github.com/pydanty)\[bot] in [#&#8203;6814](pydantic/pydantic-ai#6814)
- Point offline web UI hosting at the self-contained chat UI build by [@&#8203;dsfaccini](https://github.com/dsfaccini) in [#&#8203;7349](pydantic/pydantic-ai#7349)
- Allow adaptive thinking with Tool Output and forced tool choice on Anthropic by [@&#8203;pydanty](https://github.com/pydanty)\[bot] in [#&#8203;7200](pydantic/pydantic-ai#7200)
- Gate `RetryPromptPart` OpenTelemetry content on `include_content` by [@&#8203;sean-kim05](https://github.com/sean-kim05) in [#&#8203;7357](pydantic/pydantic-ai#7357)

**Full Changelog**: <pydantic/pydantic-ai@v2.27.0...v2.27.1>

</details>

<details>
<summary>Kludex/uvicorn (uvicorn)</summary>

### [`v0.52.4`](https://github.com/Kludex/uvicorn/releases/tag/0.52.4): Version 0.52.4

[Compare Source](Kludex/uvicorn@0.52.3...0.52.4)

##### Fixed

- Remove duplicate `Date` headers from accepted WebSocket handshakes with `websockets-sansio` ([#&#8203;3078](Kludex/uvicorn#3078))

**Full Changelog**: <Kludex/uvicorn@0.52.3...0.52.4>

</details>

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this MR, check this box

---

This MR has been generated by [Mend Renovate CLI](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4zOS4yIiwidXBkYXRlZEluVmVyIjoiNDQuOTAuMiIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIiwicmVub3ZhdGUiXX0=-->

See merge request swiss-armed-forces/cyber-command/cea/loom!769

Co-authored-by: Loom MR Pipeline Trigger <group_103951964_bot_9504bb8dead6d4e406ad817a607f24be@noreply.gitlab.com>
Co-authored-by: shrewd-laidback palace <shrewd-laidback-palace-736-c41-2c1-e464fc974@swiss-armed-forces-open-source.ch>
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.

Smart union selects the wrong model in JSON mode with a before validator

3 participants