fix(contracts): make DataContractRead.model_validate accept ORM rows (#455)#457
Merged
Merged
Conversation
2462f44 to
3a62825
Compare
dc0a883 to
a0d2ad0
Compare
3a62825 to
ce1f16b
Compare
3 tasks
…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.
a0d2ad0 to
fe28ad8
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #455 —
POST /api/data-contracts/{id}/clonereturned 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:
DataContractRead.model_validate(orm_row).model_dump()withoutfrom_attributes=True. Pydantic v2 does not auto-propagate the parent model's ORM-mode config into nested model validation when the parent is validated viamodel_validate(obj)(only when the kwarg is passed at the call site). The nestedServerConfigtherefore tried to coerce aDataContractServerDbORM row as a dict and raisedInput should be a valid dictionary or instance of ServerConfig→ caught by the genericexcept ValueError as e→ 400.from_attributes=Truepropagating in,ServerConfig.properties(typedDict[str, Any]) cannot validate the SQLAlchemyInstrumentedListofDataContractServerPropertyDbrows 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: passfrom_attributes=Trueto all fourDataContractRead.model_validate(...)call sites (clone, version history's current/parent/children/siblings, personal drafts).models/data_contracts_api.py:from_attributes = TruetoServerConfig.Configso future callers don't have to remember the kwarg.@field_validator('properties', mode='before')that flattens a list of{key, value}rows or dicts into the expectedDict[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 populatedservers,team,roles,support,pricing, andsla_propertiescollections.ServerConfig.propertiescoercion 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, andtest_version_family.pycontinue 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 tomain.Test plan
pytest src/tests/unit/test_data_contracts_api_models.py)id/versionand populatedserversinstead of 400.current/parent/children/siblingsall serialize cleanly.