Skip to content

feat: generate response types from the API spec - #28

Merged
AminChirazi merged 1 commit into
mainfrom
feat/generated-response-types
Jul 29, 2026
Merged

feat: generate response types from the API spec#28
AminChirazi merged 1 commit into
mainfrom
feat/generated-response-types

Conversation

@AminChirazi

Copy link
Copy Markdown
Collaborator

Every route method in this package was annotated -> Dict or -> List[Dict]. That tells a type checker nothing and an IDE less. The API's OpenAPI document now describes 209 of its 240 operations precisely, so the shapes are generated from it rather than hand-written — the arrangement datamaker-js already uses, and the one that stops an SDK drifting behind its API.

spec/openapi.json  ->  src/datamaker/generated/schema.py  ->  src/datamaker/types.py

93 methods across 15 route modules now return a named type.

Non-breaking by construction

TypedDict, not pydantic. Methods still return the plain dicts response.json() produces, so project["name"] works exactly as before and no existing code changes behaviour. The types are checker-only. 69 tests pass unchanged.

Why there is an alias module

This is the part worth reviewing.

datamodel-code-generator lets an inline schema claim a bare name and silently renames the real component. The generated class called Template is an unrelated nested object from DatabaseTemplatesProposal; the actual Template entity is Template2. Six of 123 schemas were affected — including Plan, Scenario and Template, the three this SDK uses most.

My first pass imported those blind and shipped exactly the failure this work exists to prevent: a wrong type that type-checks. mypy caught it (TypedDict "Template" has no key "id"), and an audit of all 123 schemas against their generated classes showed the full extent.

So scripts/generate_schema.py now derives src/datamaker/types.py by matching key sets against the spec on every run, and fails if any schema stops resolving uniquely. Nothing hand-written ever names a numbered class, and the unstable numbering cannot leak into the package.

# src/datamaker/types.py - generated
Plan = _s.Plan1
Scenario = _s.Scenario1
Template = _s.Template2

What is deliberately still Dict

Three schemas cannot be generated: SetDetail, PackInstallListItem and SchemaGraphNavigation are composed with allOf (zod's .extend()), which the generator merges away instead of emitting as a named class. Methods returning those keep Dict. The list is explicit in the script and the audit fails if it grows, so this cannot quietly become four.

The other untyped methods are the ones the API deliberately leaves bodyless upstream — connections, /endpoints/auth-resolve, /export/rest — which return credentials or third-party bodies.

Bugs found on the way, fixed rather than silenced

  • read_file_by_path returns raw bytes, not the metadata row its endpoint answers with. My endpoint-matcher got this wrong and mypy caught it.
  • generate_from_template_id was writing a quantity key onto a fetched Template. It now copies ({**template, "quantity": quantity}) — identical behaviour, minus writing a field back onto a response object the API never sends.
  • Two pre-existing bugs in connections.py: a bool assigned into a dict mypy infers as str-valued.

Net mypy: 8 errors → 6. Nothing introduced. The remaining 6 are pre-existing (implicit-Optional defaults and two Literal arg-types in main.py) and left alone.

Verification

  • pytest -m "not integration" — 69 passed, 24 skipped
  • python scripts/generate_schema.py --check — clean; verified it fails on a hand-edit
  • mypy src/datamaker — 6, all pre-existing, diffed line-number-insensitively against main
  • uv build — sdist + wheel

CI gains the drift check and, for the first time, actually runs the testsbuild.yaml only ran uv build before.

Follow-up worth considering

The three allOf schemas would generate cleanly if the API declared them as standalone objects rather than .extend(). That is a small upstream change in apps/datamaker-api/src/schemas/entities.ts and would benefit datamaker-js too.

Companion to automators-com/datamaker-js#21.

🤖 Generated with Claude Code

Every route method was annotated `-> Dict` or `-> List[Dict]`, which tells
a type checker nothing and an IDE less. The API's OpenAPI document now
describes 209 of its 240 operations precisely, so the shapes are generated
from it rather than hand-written - the arrangement datamaker-js already
uses, and the one that stops an SDK drifting behind its API.

  spec/openapi.json -> src/datamaker/generated/schema.py -> src/datamaker/types.py

93 methods across 15 route modules now return a named type. TypedDicts,
not pydantic: methods still return the plain dicts `response.json()`
produces, so `project["name"]` works exactly as before and no existing
code breaks. The types are checker-only.

WHY THERE IS AN ALIAS MODULE. datamodel-code-generator lets an inline
schema claim a bare name and renames the real component: the class called
`Template` is an unrelated nested object from DatabaseTemplatesProposal,
while the actual Template entity is `Template2`. Six of 123 schemas were
affected, including Plan, Scenario and Template - the three this SDK uses
most. Importing those blind would have shipped exactly the failure this
work exists to prevent: a wrong type that type-checks. So the generator
now derives `types.py` by matching KEY SETS against the spec on every run,
and fails if any schema stops resolving uniquely. Nothing hand-written
ever names a numbered class.

Three schemas still cannot be generated - SetDetail, PackInstallListItem
and SchemaGraphNavigation are composed with allOf, which the generator
merges away instead of emitting. Those methods keep `Dict`. The list is
explicit in the script and the audit fails if it grows.

mypy found two mis-annotations while this was being written, both fixed
rather than silenced: `read_file_by_path` returns raw bytes, not the
metadata row its endpoint answers with, and `generate_from_template_id`
was writing a `quantity` key onto a fetched Template. The latter now
copies instead of mutating - identical behaviour, minus writing a field
back onto a response object that the API never sends.

Net mypy: 8 errors -> 6. Nothing introduced; two pre-existing bugs in
connections.py fixed (a bool assigned into a dict inferred str-valued).

CI gains the drift check and, for the first time, actually runs the tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 29, 2026 06:03

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@AminChirazi
AminChirazi merged commit ca05b5e into main Jul 29, 2026
6 checks passed
@AminChirazi AminChirazi mentioned this pull request Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants