Every request model in server/models.py sets model_config = ConfigDict(extra="forbid") except the four the inference slices added. On those four, a body carrying a field the model does not declare is silently ignored instead of refused.
| Model |
extra |
AnnotationCreate, AnnotationUpdate |
forbid |
BatchCreate, ProjectCreate, ReleaseCreate, SchemaVersionCreate |
forbid |
ConnectionCreate, ConnectionUpdate |
(pydantic default — ignore) |
SuggestPoint, SuggestRequest |
(pydantic default — ignore) |
Found while removing the dead detail field (cf. #463), where the question "what does this wire do with an unexpected field?" had to be answered before the removal could be called behaviour-preserving. Nothing in the code or the docstrings records the omission as deliberate — the six models that forbid carry a bare model_config line with no comment, so there is no stated rule the four are an exception to.
Why it is worth closing
forbid is what turns a client's typo into a 422 instead of a silent no-op. On these four routes:
{"model_revision": "..."} misspelled on a connection update is accepted, changes nothing, and answers 200 with the old value — the caller has no way to tell it did not take.
- A field removed from the contract goes on being accepted forever, which is the shape
#463 was in: detail had been accepted and ignored since it shipped.
The second point cuts both ways and is the reason this is worth a decision rather than a reflex. Because SuggestRequest ignores extras today, removing detail was invisible to any caller still sending it. Tightening to forbid would make that same caller start getting a 422 — so the change is only free while no client depends on the laxness.
What it needs
A decision on whether the four join the convention, and if so a ConfigDict(extra="forbid") on each plus one test per model proving an undeclared field is refused. openapi.json moves too — forbid emits additionalProperties: false — so the contract and the generated client are part of the diff.
Every request model in
server/models.pysetsmodel_config = ConfigDict(extra="forbid")except the four the inference slices added. On those four, a body carrying a field the model does not declare is silently ignored instead of refused.extraAnnotationCreate,AnnotationUpdateforbidBatchCreate,ProjectCreate,ReleaseCreate,SchemaVersionCreateforbidConnectionCreate,ConnectionUpdateSuggestPoint,SuggestRequestFound while removing the dead
detailfield (cf. #463), where the question "what does this wire do with an unexpected field?" had to be answered before the removal could be called behaviour-preserving. Nothing in the code or the docstrings records the omission as deliberate — the six models that forbid carry a baremodel_configline with no comment, so there is no stated rule the four are an exception to.Why it is worth closing
forbidis what turns a client's typo into a 422 instead of a silent no-op. On these four routes:{"model_revision": "..."}misspelled on a connection update is accepted, changes nothing, and answers 200 with the old value — the caller has no way to tell it did not take.#463was in:detailhad been accepted and ignored since it shipped.The second point cuts both ways and is the reason this is worth a decision rather than a reflex. Because
SuggestRequestignores extras today, removingdetailwas invisible to any caller still sending it. Tightening toforbidwould make that same caller start getting a 422 — so the change is only free while no client depends on the laxness.What it needs
A decision on whether the four join the convention, and if so a
ConfigDict(extra="forbid")on each plus one test per model proving an undeclared field is refused.openapi.jsonmoves too —forbidemitsadditionalProperties: false— so the contract and the generated client are part of the diff.