basecamp / basecamp/basecamp-sdk

TypeScript, Python and Ruby have no runtime response validation on any route

Open
#578 1 comment 0 reactions 0 assignees View on GitHub
enhancement python ruby typescript
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.