Skip to content

fix(api): response schemas declare properties the server never returns (611 baselined example gaps) #659

Description

@ericfitz

Summary

Adding the check-response-examples gate (#657 remediation) surfaced 611 properties across 93
operations
that a 2xx response schema declares but the operation's example omits. None of them
fires in CATS today — each was checked against the 107,608-response corpus from run
20260731T200651Z and never observed — so they are recorded in
api-schema/response-example-baseline.json rather than blocking the build.

They are still debt, and they are two different bugs wearing the same shirt:

  1. The schema is wrong — it declares a shape the server does not return.
  2. The operation is simply unexercised — the property is real but no fuzzer has produced it yet,
    so it is a latent finding waiting for coverage to shift. This is exactly how
    PATCH /teams/{team_id} went from invisible to 54 findings in one run.

Only case 2 should be fixed by completing the example. Completing case 1 would document fields the
endpoint never emits, which is worse than the gap.

Confirmed instance of case 1: Project.team

Project.team is declared as a full Team:

"team": { "allOf": [{"$ref": "#/components/schemas/Team"}], "nullable": true, "readOnly": true }

The server returns only a summary:

"team": {"id": "6a6d2eb6-dd45-4f51-9799-53a4fc5ccb7d", "name": "CATS Test Team"}

team.created_at appears in 0 of 107,608 responses. This accounts for the largest baseline
entries (POST /projects 24, PATCH /projects/{project_id} 24, PUT /projects/{project_id} 23,
GET /projects/{project_id} 22).

The 2026-07-30 session already noticed this and deliberately documented team as the {id, name}
summary it actually is — the right call for docs, but it leaves the schema asserting something
untrue. A client generating types from the spec gets a Team and finds most of it undefined.

Fix: introduce a TeamSummary schema ({id, name}) and point Project.team at it. This changes
generated types in api/api.go, so it needs a deliberate decision rather than a drive-by edit.

Baseline top entries

n operation
39 GET /threat_models/{threat_model_id}
24 POST /projects
24 PATCH /projects/{project_id}
23 PUT /projects/{project_id}
23 GET /threat_models/{threat_model_id}/diagrams/{diagram_id}
23 PUT /threat_models/{threat_model_id}/diagrams/{diagram_id}
22 GET /projects/{project_id}
16 POST /threat_models/{threat_model_id}/restore

Known-unsatisfiable, do not try to fix

Two response shapes are untyped objects whose keys are runtime-dependent, so no example can
enumerate them. These are the same class as the residual documented in #657 and are not in the
baseline (they are not declared properties at all):

  • MinimalDiagramModel.metadataadditionalProperties: {type: string}, unbounded keys.
    4 findings on GET /threat_models/{id}/diagrams/{id}/model.
  • RollbackResponse.restored_entity{"type": "object"} holding whichever entity type was rolled
    back. Its example documents a threat model; the observed response was a diagram
    (alias, cells, metadata, name, type). 4 findings.

Enumerating every restorable entity's properties into one example would satisfy CATS but produce a
nonsense document. Left alone deliberately.

Acceptance Criteria

  • Decide on TeamSummary for Project.team, or widen the server to return a full team
  • Walk the baseline and split remaining entries into "schema wrong" vs "unexercised"
  • Complete examples for the "unexercised" group so they stop being latent findings
  • Burn the baseline down; the check is a ratchet, so it can only shrink
  • Consider whether RollbackResponse.restored_entity should be a discriminated union

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    apiAPI design and implementation

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions