basecamp / basecamp/basecamp-sdk
TypeScript, Python and Ruby have no runtime response validation on any route
- Dominant language
- Go
- Stars
- 49
- Forks
- 12
- Avg merge
- 20h 47m
- Merged PRs (30d)
- 89
Description
Split out of #574's closeout, after a wrong claim on #544 was corrected: a flat Smithy shape does **not** give these three SDKs a decoder. They have never had one, on any route.
Verified on `origin/main` against a flat model with no `oneOf` near it (`todos.get`):
| SDK | Generated return | Runtime validation |
|---|---|---|
| TypeScript | `Promise`, where `Todo` aliases `components["schemas"]["Todo"]` | **none** — erased at build time |
| Python | `-> dict[str, Any]` | **none** — a raw dict |
| Ruby | `http_get(...).json` | **none** — a raw Hash |
| Go | `json.Unmarshal` into a struct | rejects wrong types |
| Kotlin | kotlinx.serialization into a data class | rejects wrong types |
| Swift | `Codable` | rejects wrong types |
## Why it matters
#574 found **eleven** real defects across eleven review passes, all one class — a value read off the wire being written back without validation — and every single one appeared in whichever language lacked a runtime decoder. Go, Kotlin and Swift needed a guard at no level. TypeScript, Python and Ruby needed one at every level, including cases that did not raise but *lied*: Ruby's `body["name"]` on a String is a substring match returning nil, and TypeScript's `"todolist" in response` on an array returns false and falls through.
The guards #574 landed are hand-written, per-composite, and only cover the routes someone thought to guard. Every future composite re-opens the same surface, and the eleven-pass history shows how easily a rung is missed.
## Scope
Give the three untyped SDKs a validating layer at the **generated** boundary, so correctness stops depending on each composite author remembering:
- reject a response body that is not the modelled shape (object vs scalar/array/null);
- reject a required field that is absent, null, or the wrong type;
- surface those as the SDK's own statusless, non-retryable `api_error` with the field named and the message capped, per SPEC §6/§9 — the contract #574 established;
- never let the error path itself throw (`JSON.stringify` on a circular structure, `repr`/`inspect` raising).
Prior art in-tree: #574's `writableString`, `requireTodolistObject` and the arm check are effectively a hand-rolled decoder for one route. Generating that per model is the general form.
## Not this issue
- #544 (flat-shape read repair) removes the envelope-arm level and the Kotlin/Swift `oneOf` machinery. It does not address this.
- #576 covers the same lenient-read shape in the *shipped* Todos/Cards composites, as targeted fixes.
Once this lands, #574's guards and #576's fixes become deletable — which is what I incorrectly claimed #544 would do.
Contributor guide
Research direction
Start by comparing the generated response paths for the flat todos.get model in TypeScript, Python, and Ruby with #574's writableString, requireTodolistObject, and arm checks. Read SPEC §6/§9 and trace how each SDK surfaces api_error. Done means the generated boundary validates all modelled routes and preserves the stated non-retryable error contract without throwing in its error path.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python, ruby, typescript
- Domain
- api
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 28/100