Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 37 additions & 7 deletions src/rapidata/api_client/lazy_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,24 @@ def __init__(self, **data: Any) -> None:
# for the vast majority of instances that validated cleanly.
object.__setattr__(self, "_field_validation_errors", {})

# ------------------------------------------------------------------
# Strict fields – any error touching one of these re-raises instead
# of lazy-constructing. Only the `_t` discriminator is strict: it's
# a structural requirement for oneOf/anyOf disambiguation (the wrong
# `_t` means the wrong variant was picked, not that a field drifted).
# Every other backend-enforced constraint (enums, regex patterns,
# etc.) stays lazy, which is the whole point of this base class.
# ------------------------------------------------------------------
@classmethod
def _strict_field_names(cls) -> set:
model_fields = getattr(cls, "model_fields", None)
if not model_fields:
return set()
for field_name, field_info in model_fields.items():
if field_info.alias == "_t":
return {field_name}
return set()

# ------------------------------------------------------------------
# Fallback construction – called when model_validate raises
# ------------------------------------------------------------------
Expand All @@ -47,7 +65,13 @@ def _lazy_construct(
data: Dict[str, Any],
error: ValidationError,
) -> "LazyValidatedModel":
"""Build the model via ``model_construct`` and store per-field errors."""
"""Build the model via ``model_construct`` and store per-field errors.

Re-raises the original ``ValidationError`` if the error targets the
`_t` discriminator field. A bad `_t` means oneOf/anyOf disambiguation
picked the wrong variant, which is a structural failure — not the
kind of backend drift lazy validation is meant to absorb.
"""

# --- alias → python field name mapping ---
alias_to_field: Dict[str, str] = {}
Expand All @@ -56,12 +80,6 @@ def _lazy_construct(
alias_to_field[alias] = field_name
alias_to_field[field_name] = field_name

# --- build kwargs with python names for model_construct ---
construct_kwargs: Dict[str, Any] = {}
for key, value in data.items():
python_name = alias_to_field.get(key, key)
construct_kwargs[python_name] = value

# --- extract per-field errors keyed by python name ---
field_errors: Dict[str, Any] = {}
for err in error.errors():
Expand All @@ -71,6 +89,18 @@ def _lazy_construct(
python_name = alias_to_field.get(alias_key, alias_key)
field_errors[python_name] = err

# --- if any strict field failed, the whole model is wrong – re-raise ---
strict_fields = cls._strict_field_names()
strict_violations = strict_fields & field_errors.keys()
if strict_violations:
raise error

# --- build kwargs with python names for model_construct ---
construct_kwargs: Dict[str, Any] = {}
for key, value in data.items():
python_name = alias_to_field.get(key, key)
construct_kwargs[python_name] = value

# --- observability: log error + fail the trace ---
error_fields = list(field_errors.keys())
logger.warning(
Expand Down
Loading