fix: make _lazy_construct respect discriminator fields#539
Conversation
Code Review: PR #539 — fix oneOf/anyOf 'Multiple matches found' regressionOverviewThis PR correctly fixes a regression from #537 where Correctness — Approved with caveatsRoot cause analysis is accurate. The disambiguation loop depended on The discriminator path is intentionally left as anyOf vs. oneOf semantics are handled correctly. anyOf exits on the first match (early Issues and Risks1. Lazy protection (#537) is partially surrendered for matched variants in a oneOf/anyOf context (important)
2. No regression test added The test plan lists manual checks but no automated test is included. For a fix to a serialization regression, a unit test would be valuable:
Given that CI isn't passing yet, and the fix touches 195 generated call sites, a targeted test would give confidence that the fix holds. 3. 195 hand-patched call sites across 37 files — maintenance risk The PR correctly notes that auto-regen is blocked by upstream schema-name conflicts. The risk is that when regen is unblocked, these changes must come from the updated templates — not be dropped. Consider adding a comment to the 4. Duplicate imports in generated files In # line 18
from pydantic import BaseModel, ConfigDict, Field, StrictStr, ValidationError, field_validator
# line 25 (duplicate)
from pydantic import StrictStr, FieldThis is a generated-code artifact, but it's worth cleaning up in the template to avoid linter warnings. 5. Verbose comments in templates (minor) The added comments span 5–6 lines explaining the # strict probe: lazy fallback in from_json would make every variant succeed
instance.actual_instance = {{{dataType}}}.model_validate_json(json_str)SummaryThe fix is technically correct and unblocks the Recommend:
|
oneOf/anyOf disambiguation relies on non-matching variants RAISING so
that only the correct variant bumps the match counter. After lazy
validation landed, every variant's from_dict caught ValidationError
and fell back to _lazy_construct, producing 'Multiple matches found'
errors on every oneOf deserialization (e.g. display_progress_bar
through GetPipelineByIdResult -> IArtifactModel).
Rather than bypass lazy in the oneOf loop (which would lose drift
tolerance for the picked variant), this change teaches
_lazy_construct to distinguish 'this variant is wrong' from
'a field drifted':
- A 'strict field' is any field with a custom Pydantic field_validator.
The OpenAPI generator emits one on every discriminator field (_t)
that enforces the enum value ('FileArtifactModel' etc.).
- If any validation error targets a strict field, the whole model is
wrong for this payload → re-raise the original ValidationError.
- Otherwise (type drifts, missing optional etc.), lazy-construct as
before and defer errors to field-access time.
Result: oneOf disambiguation works again, and a oneOf-wrapped variant
with drift on a non-discriminator field still gets lazy protection —
the variant is picked via its _t, and reading a drifted unused field
is silent while reading the drifted field raises TypeError.
Verified with 6 scenarios: valid payload, drift on non-strict field,
discriminator mismatch, oneOf happy path, oneOf + drifted non-strict
field, and oneOf gibberish.
6756f88 to
0e392e6
Compare
Code ReviewOverviewThis PR fixes a regression from #537 where all oneOf/anyOf variants appeared to match during deserialization, causing a "Multiple matches found" error. The root cause: lazy validation in The fix is elegant — instead of patching 195 call sites (the original plan described), it teaches What the fix actually does (vs. what the PR description says)The PR description's "What changed" section lists three items:
None of these appear in the diff. The actual diff is 39 lines entirely in Code qualityStrengths:
Issues: 1. Relies on private Pydantic internals — silent failure riskdecorators = getattr(cls, "__pydantic_decorators__", None)
if decorators is None:
return set() # silently falls back — no strict fields detected
2.
|
Previous rule made every @field_validator field strict (enum and regex validators included), which reintroduced the brittleness lazy validation exists to avoid. The only field that must stay strict is _t, because oneOf/anyOf disambiguation depends on it. Enum/pattern drift on any other field now falls back to lazy construction, as originally intended.
Code ReviewOverviewThis PR fixes a regression from #537 where lazy validation caused all oneOf/anyOf variants to "succeed" during disambiguation (since Code Quality
Potential Issues
Test CoverageThe PR description lists 6 "Verified scenarios" that read like manual test cases. None of them appear as automated tests in the diff. Given that this is a regression fix, adding a small unit test covering at minimum scenarios 3 and 4 (wrong Summary
The core fix is correct and well-reasoned. The main asks before merge are:
|
Summary
Fixes the
ValueError: Multiple matches found when deserializing the JSON string into IArtifactModel with oneOf schemas: ...regression introduced by #537 (lazy validation), reported on thedisplay_progress_barflow (GetPipelineByIdResult.from_dict -> IArtifactModel.from_dict -> IArtifactModel.from_json).Root cause
oneOf/anyOf disambiguation loops in the generated code look like:
This relies on non-matching variants raising
ValidationError. After #537,Variant.from_json -> from_dictcatchesValidationErrorand falls back to_lazy_construct, so every variant "succeeds",match > 1, and the branch raises "Multiple matches found".Fix
Distinguish "this variant is wrong" from "a field drifted" inside
_lazy_constructitself:field_validator. The OpenAPI generator already emits one on every discriminator field (_t) that enforces the enum value ("FileArtifactModel"etc.).ValidationError. The model choice is wrong, not drifting.No template or generated-file changes — the discriminator handling is fully in the base class, auto-detected via
__pydantic_decorators__.field_validators.Why this is better than bypassing lazy in the oneOf loop
An earlier version of this PR patched the oneOf/anyOf templates to use
model_validate_json(strict) in the disambiguation loop. That worked but lost lazy protection for fields inside the picked variant — drift on an unused field of the chosen oneOf variant would crash the deserialization entirely.The strict-discriminator approach preserves the lazy benefit everywhere: once
_tpicks the right variant, any non-discriminator drift inside that variant is still absorbed.Verified scenarios
TypeErroron bad field access_t— re-raisesValidationErrorIArtifactModel.from_dictvia oneOf (the reported bug) — picks correct variantFiles touched
src/rapidata/api_client/lazy_model.py— one method added (_strict_field_names), one early-return added in_lazy_construct.No template changes. No generated-file changes.