Skip to content

fix(generator): resolve $ref'd response and request body component types - #309

Draft
mfal wants to merge 2 commits into
claude/das-repo-unit-tests-23fb42from
claude/fix-ref-response-body-types
Draft

mfal wants to merge 2 commits into
claude/das-repo-unit-tests-23fb42from
claude/fix-ref-response-body-types

Conversation

@mfal

@mfal mfal commented Sep 8, 2026

Copy link
Copy Markdown
Member

Stacked on #306, which added the tests that pin down the old behaviour. Base this on #306, not master — it will need retargeting once #306 lands.

The bug

A path response that $refs a component response compiled to an index signature:

namespace $400 {
  namespace Content {
    export interface ApplicationJson {
      [k: string]: unknown;   // 👈
    }
  }
}

ResponseContentTypes.buildContentTypesFromReferenceObject handed ResponseContent a JSONSchema model instance where a raw schema object was expected. The instance got wrapped in a second JSONSchema, so schemaObject was a model instance rather than a schema and json-schema-to-typescript fell back to its index-signature default.

Every error response in the mittwald spec is such a $ref:

"400": { "$ref": "#/components/responses/de.mittwald.v1.commons.ValidationError" }

so every error payload in the published @mittwald/api-client was untyped — even though Components.Responses.CommonsValidationError.ApplicationJson was generated correctly right next to it. Consumers calling assertStatus(response, 400) got unknown fields.

The fix

asCustomTypeRef(), not schemaObject. Both cure the index signature, but asCustomTypeRef() emits a reference to the already-generated component type:

export type ApplicationJson =
  MittwaldAPIV2.Components.Responses.CommonsValidationError.ApplicationJson;

schemaObject would instead inline the component's underlying schema. That only works at all when the component response's schema is itself a \$ref, and where it does work it duplicates the schema at every referencing path — ~4000 copies here. Referencing the component also matches how the path-level request body already resolves (RequestBody = Components.RequestBodies.PetBody), and is exactly the output the old test's not.toContain predicted.

RequestBodies had the same symptom from a different cause: a request body component is a RequestBodyObject, not a schema — the schema sits under content[mediaType].schema — so the whole wrapper (content, required: true) was being compiled. It is now unwrapped, preferring application/json since a component yields a single type and cannot name one per media type. The mittwald spec declares no request body components, so this half only shows up in the fixture.

Tests

Both tests now assert the resolved types. The second one is replaced by a blanket not.toContain("[k: string]: unknown;") — nothing in the fixture should compile to an index signature, which guards the whole class of bug rather than the two known instances.

Regenerated client

I regenerated on the unmodified base first to prove it was clean (empty diff), so the diff here is purely this change:

packages/mittwald/src/generated/v2/types.ts      | 9760 +++++-------
packages/mittwald/src/generated/v3-next/types.ts | 9760 +++++-------

Every changed line is the same substitution. 3896 index signatures across the two clients became typed references:

Component response Occurrences (per client)
Commons*DefaultError 903
Commons*RateLimitError 473
Commons*ValidationError 299
Commons*NotFoundError 264
others (SslValidationError, DomainSuccessResponse, …) 9

Note this is a type-level widening from unknown to concrete fields, so it can surface pre-existing type errors in consumer code that was previously indexing these payloads freely.

Not addressed here

Paths.V2Files.Post.Parameters.RequestBody is still an index signature, for an unrelated reason: RequestParameters.fromDoc looks only at content["application/json"], and POST /v2/files declares only multipart/form-data, so it falls through to the raw RequestBodyObject. That is the subject of #300, so I left it rather than collide. The 6 remaining index signatures are genuine free-form schemas in the spec (expiresAt: {}, audit-log changes).

Verification

  • @mittwald/api-code-generator:test:unit — 145/145 pass
  • nx run-many -t test:unit — all pass
  • nx run-many -t test:compile --parallel=3 --skip-nx-cache — all pass
  • nx run-many -t lint — clean

test:client-generation-clean is only git diff --exit-code and proves nothing about generation, so the generated output was verified by regenerating and reading the diff.

🤖 Generated with Claude Code

mfal and others added 2 commits September 8, 2026 15:09
A path response that `$ref`s a component response compiled to
`{ [k: string]: unknown }`: `buildContentTypesFromReferenceObject` handed
`ResponseContent` a `JSONSchema` _model instance_ where a raw schema object
was expected, so the instance was wrapped in a second `JSONSchema` and
`json-schema-to-typescript` had nothing to work with. It now passes
`asCustomTypeRef()`, which references the component response's own generated
content type instead of rebuilding it -- the same indirection the path-level
request body already uses, and the only option that does not duplicate an
inline component response at every referencing path.

Every error response in the mittwald spec is such a `$ref`, so this left every
error payload in the published client untyped. Regenerating turns 3896 index
signatures across v2 and v3-next into references to
`Components.Responses.Commons*Error.ApplicationJson`; consumers doing
`assertStatus(response, 400)` now get real fields. The generated diff contains
nothing else -- regenerating the unmodified base produces no diff at all.

`RequestBodies` had the same symptom from a different cause: a request body
component is a `RequestBodyObject`, not a schema, so compiling the wrapper
(`content`, `required: true`) also yielded an index signature. The schema is
now unwrapped from `content[mediaType].schema`, preferring `application/json`
because a component produces a single type and cannot name one per media
type. The mittwald spec declares no request body components, so this only
shows up in the fixture.

The two tests that pinned the old behaviour now assert the resolved types.
The second one is replaced by a blanket assertion that no type in the fixture
compiles to an index signature, which guards the class of bug rather than the
two known instances.

Not addressed here: `Paths.V2Files.Post.Parameters.RequestBody` is still
untyped, because `RequestParameters.fromDoc` only looks at
`content["application/json"]` and `POST /v2/files` declares only
`multipart/form-data`. That is the subject of #300.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…response-body-types

The base branch picked up master, which regenerated the committed client;
both sides had therefore rewritten the generated types. Resolved by taking
the base branch's client and regenerating it with this branch's generator
fix, so the $ref'd response and request body types resolve again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant