Skip to content

fix(contracts): make DataContractRead.model_validate accept ORM rows (#455)#457

Merged
larsgeorge-db merged 1 commit into
mainfrom
fix-clone-serverconfig-serialization
May 28, 2026
Merged

fix(contracts): make DataContractRead.model_validate accept ORM rows (#455)#457
larsgeorge-db merged 1 commit into
mainfrom
fix-clone-serverconfig-serialization

Conversation

@larsgeorge-db
Copy link
Copy Markdown
Collaborator

Summary

Closes #455POST /api/data-contracts/{id}/clone returned HTTP 400 ("Invalid contract data") even though the cloned version was successfully written to the database. The bug was in response serialization, not the clone logic itself.

Root cause is a two-layer Pydantic v2 incompatibility:

  1. The clone/history/personal-drafts routes call DataContractRead.model_validate(orm_row).model_dump() without from_attributes=True. Pydantic v2 does not auto-propagate the parent model's ORM-mode config into nested model validation when the parent is validated via model_validate(obj) (only when the kwarg is passed at the call site). The nested ServerConfig therefore tried to coerce a DataContractServerDb ORM row as a dict and raised Input should be a valid dictionary or instance of ServerConfig → caught by the generic except ValueError as e → 400.
  2. Even with from_attributes=True propagating in, ServerConfig.properties (typed Dict[str, Any]) cannot validate the SQLAlchemy InstrumentedList of DataContractServerPropertyDb rows that the relationship returns — those are key/value rows, not a dict. This was a latent second-layer bug that fix Cannot Create a New Domain if Choosing a Parent One #1 exposed.

Changes

  • routes/data_contracts_routes.py: pass from_attributes=True to all four DataContractRead.model_validate(...) call sites (clone, version history's current/parent/children/siblings, personal drafts).
  • models/data_contracts_api.py:
    • Add from_attributes = True to ServerConfig.Config so future callers don't have to remember the kwarg.
    • Add a @field_validator('properties', mode='before') that flattens a list of {key, value} rows or dicts into the expected Dict[str, Any]. Plain-dict input is passed through unchanged.

Tests

src/backend/src/tests/unit/test_data_contracts_api_models.py (+184 lines):

  • DataContractRead.model_validate(orm_row, from_attributes=True) now round-trips populated servers, team, roles, support, pricing, and sla_properties collections.
  • ServerConfig.properties coercion exercised directly for object-rows (.key/.value), plain dict, and list-of-{key, value} dicts.

```
======================= 9 passed, 112 warnings in 0.32s ========================
```

All 42 tests across test_data_contracts_api_models.py, test_version_visibility.py, and test_version_family.py continue to pass.

Stacking

This PR is stacked on PR #448 (PRD #442 phase 3, branch feat-version-family-picker). Once #448 merges to main, please retarget this PR's base to main.

Test plan

  • Unit tests pass (pytest src/tests/unit/test_data_contracts_api_models.py)
  • Manual smoke: clone a contract via UI on the version-family worktree and confirm the response body now contains the cloned id/version and populated servers instead of 400.
  • Manual smoke: open the version history dialog for a contract with servers and confirm current/parent/children/siblings all serialize cleanly.

@larsgeorge-db larsgeorge-db requested a review from a team as a code owner May 27, 2026 21:30
@larsgeorge-db larsgeorge-db force-pushed the feat-version-family-picker branch from 2462f44 to 3a62825 Compare May 28, 2026 15:08
@larsgeorge-db larsgeorge-db force-pushed the fix-clone-serverconfig-serialization branch from dc0a883 to a0d2ad0 Compare May 28, 2026 15:08
@larsgeorge-db larsgeorge-db force-pushed the feat-version-family-picker branch from 3a62825 to ce1f16b Compare May 28, 2026 15:11
@larsgeorge-db larsgeorge-db changed the base branch from feat-version-family-picker to main May 28, 2026 15:11
@larsgeorge-db larsgeorge-db changed the base branch from main to feat-version-family-picker May 28, 2026 15:12
…455)

POST /api/data-contracts/{id}/clone returned HTTP 400 ("Invalid contract
data") even though the cloned version was successfully written to the
database. Root cause was a two-layer Pydantic v2 incompatibility in the
response serialization path:

1. `DataContractRead.model_validate(new_contract).model_dump()` did not
   pass `from_attributes=True`, so the flag was not propagated into nested
   model validation. The nested `ServerConfig` (no `from_attributes`)
   then rejected the ORM `DataContractServerDb` rows it received via the
   `servers` relationship -> ValidationError -> caught as 400.
2. Even with `from_attributes=True` propagating, `ServerConfig.properties`
   (typed `Dict[str, Any]`) cannot validate the SQLAlchemy
   `InstrumentedList` of `DataContractServerPropertyDb` rows that the
   relationship returns. This was a latent second-layer bug exposed
   by fix #1.

Fixes:
- Pass `from_attributes=True` at the four call sites that validate
  `DataContractRead` from ORM instances (clone, version history, personal
  drafts).
- Add `from_attributes = True` to `ServerConfig.Config` so any future
  caller validating a nested `DataContractServerDb` works without
  remembering to pass the flag.
- Add a `@field_validator('properties', mode='before')` on `ServerConfig`
  that flattens a list of `{key, value}` rows/dicts into the expected
  `Dict[str, Any]`. Plain dict input keeps working unchanged.

Regression coverage in `test_data_contracts_api_models.py`:
- `DataContractRead.model_validate(orm_row, from_attributes=True)` now
  round-trips populated `servers`, `team`, `roles`, `support`, `pricing`,
  and `sla_properties` collections.
- The `ServerConfig.properties` coercion is exercised directly for
  object-rows, plain dict, and list-of-`{key,value}` shapes.

Stacked on PR #448 (PRD #442 phase 3); closes #455.
@larsgeorge-db larsgeorge-db force-pushed the fix-clone-serverconfig-serialization branch from a0d2ad0 to fe28ad8 Compare May 28, 2026 15:13
@larsgeorge-db larsgeorge-db changed the base branch from feat-version-family-picker to main May 28, 2026 15:13
@larsgeorge-db larsgeorge-db merged commit 09987af into main May 28, 2026
@larsgeorge-db larsgeorge-db deleted the fix-clone-serverconfig-serialization branch May 28, 2026 15:13
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.

POST /data-contracts/{id}/clone returns 400 despite successful clone (Pydantic v2 ServerConfig) Cannot Create a New Domain if Choosing a Parent One

1 participant