Skip to content

fix(core): emit a typedef for a schema that is only a $ref - #10

Open
barney-ws wants to merge 1 commit into
mainfrom
fix/toplevel-ref-alias
Open

barney-ws wants to merge 1 commit into
mainfrom
fix/toplevel-ref-alias

Conversation

@barney-ws

Copy link
Copy Markdown
Collaborator

Problem

A top-level schema whose whole body is a $ref is an alias for another schema. OpenAPI 3.1 allows keywords beside a $ref, and nestjs-zod emits exactly this when a DTO is exposed under a second name:

"OpenShiftResponseDtoV2": {
  "id": "OpenShiftResponseDtoV2",
  "$ref": "#/components/schemas/OpenShiftResponseDto"
}

dorval treats such a schema asymmetrically: referring properties keep the alias name, and nothing ever generates a file for it.

// models/shift_response_dto.f.dart
import 'open_shift_response_dto_v2.f.dart';           // <- file does not exist
List<OpenShiftResponseDtoV2>? openShiftResponses,     // <- class does not exist

models/index.dart stays self-consistent (it only exports files that were written), so the breakage surfaces only through the referring models' imports. build_runner reports it as:

Could not generate `fromJson` code for `item`.
To support the type `InvalidType` you can: ...

Two places drop the alias, one after the other:

  • OpenAPIParser.getSchemas() filters out every schema carrying a $ref ("Filter out reference objects"), so the alias never reaches the model loop;
  • had it reached it, isEmpty() would have discarded it anyway — it checks type, properties, enum, const and composition, and never looks at $ref, so { id, $ref } reads as an empty schema.

Meanwhile processProperty resolves a $ref to a class name and an import without asking whether that schema produced a file.

Not a regression: the same spec produces the same 252 models on 0.10.2, 0.10.3, 0.10.7 and 0.10.8, none of them containing the aliases. It stays hidden while stale hand-written or previously-generated classes sit in the output directory, and surfaces the moment that directory is cleared.

Change

An alias generates a typedef:

// Generated typedef: this schema is an alias for another
import 'open_shift_response_dto.f.dart';
export 'open_shift_response_dto.f.dart';

typedef OpenShiftResponseDtoV2 = OpenShiftResponseDto;

The re-export is load-bearing, not decoration. freezed writes its output as a part of the referring model, and a typedef resolves to the underlying class in that part, so the class has to be in the referring file's scope. Reaching it only through the alias import leaves json_serializable seeing an InvalidType. Verified both ways, see below.

A typedef rather than a duplicated class also means the alias cannot drift from its base.

A $ref carrying real constraints beside it is an override, not a second name, and is left to the paths that already handle it. Only $ref plus pure metadata (id, title, description, example, deprecated, …) counts as an alias.

Tests

4 cases in models-ref-alias.test.ts: the 3.1 sibling-keyword form, the bare { $ref } form, self-consistency of the referring model's imports against the emitted file list, and the override form staying off this path. Three of them fail without the fix. packages/core: 454 tests pass, lint unchanged at its 3 pre-existing errors.

End-to-end through the real toolchain, with an alias referenced from a list and from a required field:

build_runner
with the re-export Built with build_runner in 4s; wrote 12 outputs, dart analyze clean
import only, no re-export Could not generate 'fromJson' code for 'item' / InvalidType / Failed to build

The generated decoders show why — the typedef is resolved to the base class inside a part of the referrer:

// models/managers_requests_item_dto.f.g.dart
item: TimeOffRequestResponseDto.fromJson(json['item'] as Map<String, dynamic>),

// models/shift_response_dto.f.g.dart
?.map((e) => OpenShiftResponseDto.fromJson(e as Map<String, dynamic>))

Notes

Reported alongside a separate config-loading problem: dorval.config.ts fails to load under TypeScript 7 with typescript.findConfigFile is not a function. That one is real and unrelated to this change — TypeScript 7's main entry exports only version and versionMajorMinor, so cosmiconfig's built-in TS loader cannot work — and searchPlaces in packages/dorval/src/config.ts lists orval.config.* rather than the dorval.config.* names the README documents. Both belong in the CLI package and will be filed separately.

A top-level schema whose whole body is a `$ref` is an alias, and OpenAPI 3.1
lets keywords sit beside the `$ref` - nestjs-zod writes `{ id, $ref }` when a
DTO is exposed under a second name. Referring properties kept naming the alias
while nothing generated a file for it, because getSchemas() drops anything
carrying a `$ref` and isEmpty() reads what is left as an empty schema. The
result was an import of a file nobody wrote and an undefined type:

    import 'open_shift_response_dto_v2.f.dart';        // no such file
    List<OpenShiftResponseDtoV2>? openShiftResponses,  // no such class

build_runner reports it as `Could not generate 'fromJson' code for ... InvalidType`.

Aliases now generate a typedef. The re-export alongside the import is
load-bearing: freezed writes its output as a `part of` the referring model and
resolves the typedef to the underlying class there, so that class has to be in
the referring file's scope. With only an import, build_runner still fails with
InvalidType.

A `$ref` carrying real constraints is an override rather than a second name,
and is left to the paths that already handle it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@barney-ws

Copy link
Copy Markdown
Collaborator Author

@qwlong @claude Please review this PR when you have time, thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant