ProjectTech4DevAI / ProjectTech4DevAI/kaapi-backend
API Audit: Strengthening type safety
@Prajna1999 is already working on this.
Since Jul 24, 2026.
- Dominant language
- Python
- Stars
- 18
- Forks
- 10
- Avg merge
- 2d 20h
- Merged PRs (30d)
- 14
Description
Describe the current behavior
We added an automated breaking-change gate in #892 (.github/workflows/api-breaking-changes.yaml) that exports the OpenAPI schema for base vs. HEAD, diffs them with oasdiff, and blocks merges that introduce breaking changes (overridable with the breaking-change-approved label).
The gate is only as good as the schema it diffs. oasdiff can only see what OpenAPI encodes. Wherever a request/response body is typed as an opaque, permissive shape — dict[str, Any], bare Any, list[dict[str, Any]], or a route with no meaningful response_model — OpenAPI renders it as a formless object with no properties and no required list. To oasdiff, such a field looks identical before and after any change to its real contents, so genuine breaking changes pass straight through the gate undetected.
Concrete example (already happened): the credential contract.
credentialsis typedlist[dict[str, Any]]inapp/models/onboarding.pyand described as a generic "Dictionary mapping provider names to their credentials" inapp/models/credentials.py.- The actual per-provider required fields live in a runtime Python dict —
PROVIDER_CONFIGSinapp/core/providers.py— enforced imperatively byvalidate_provider_credentials(). - In #890 (2026-06-11),
provider: googlewas changed so its required fields went from["api_key"]to["api_key", "project_id", "location", "sa_key", "gcs_bucket"]. Any client previously sendingprovider: googlewith onlyapi_keynow gets rejected withMissing required fields. - This is a textbook breaking change. The oasdiff gate had already been live for 9 days (#892 merged 2026-06-02). It said nothing — because the requirement was never in the schema, it was in a dict[str, Any] plus a runtime dict.
If a discriminated union had modeled the credentials (see "solution" below), OpenAPI would have carried a real required list per provider variant and oasdiff would have flagged #890 as breaking.
Describe the enhancement you'd like
A systematic audit + hardening pass across the whole API surface:
- Inventory every loose type on the request/response boundary. Starting data from a quick grep of backend/:
dict[str, Any] / Dict[str, Any]<-- a lenient dict like this may not be the only culprit, need to check for other loose types also - Triage each occurrence into:
- Fixable → model it properly. Replace with a typed Pydantic model. For fields whose shape depends on a discriminator (the credentials case), use a Pydantic discriminated union so OpenAPI emits oneOf + discriminator, each variant carrying its own required fields:
class GoogleCredentials(BaseModel):
provider: Literal["google"]
api_key: str
project_id: str
location: str
sa_key: dict
gcs_bucket: str
class OpenAICredentials(BaseModel):
provider: Literal["openai"]
api_key: str
# ...
Credential = Annotated[
Union[GoogleCredentials, OpenAICredentials, ...],
Field(discriminator="provider"),
]
- Genuinely dynamic → document + guard another way. Some payloads are legitimately free-form (arbitrary provider passthrough, user-defined metadata blobs). For these, oasdiff structurally can't help. Add a golden-file contract test instead: snapshot the effective contract (e.g. PROVIDER_CONFIGS → a checked-in providers.contract.json) and assert equality in CI, so any change fails the test with a visible diff and forces explicit review.
- Prevent regressions. Decide on a lint/CI rule that discourages introducing new dict[str, Any] / bare Any on request/response models without an explicit, reviewed opt-out, so the audit doesn't silently rot back.
Why is this enhancement needed?
- Correctness of the guardrail: right now the breaking-change gate gives false confidence: a green check does not mean "no breaking changes," it means "no breaking changes in the parts of the contract we bothered to type strictly." #890 is proof of the blind spot and we have already faced the consequences.
- Client trust: downstream consumers rely on our contracts, silent breaking failures surface as production failures
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.