Skip to content

feat: resolve record components as DTO fields - #153

Merged
tangcent merged 1 commit into
mainfrom
feature/issue-139-record-components
Sep 30, 2026
Merged

tangcent merged 1 commit into
mainfrom
feature/issue-139-record-components

Conversation

@tangcent

Copy link
Copy Markdown
Owner

Summary

Closes #139.

The Java parser only walked class_declaration and interface_declaration nodes, so a record used as a request or response DTO contributed zero fields: record components are formal_parameter nodes under the declaration's formal_parameters, not field_declaration nodes, and nothing looked for record_declaration at all. A DTO written in the now-standard form

public record CreateUserRequest(String name, String email, int age) {}

exported a request body with no fields. Records are the default DTO style in modern Java codebases, so this silently affected a large share of real projects.

This PR teaches the parser records and lets them resolve through the existing class registry and type resolver unchanged.

Changes

File Change
api-collector-java/parser/extractor.go New extractRecord reads the name, annotations, type parameters, and implements clause off record_declaration — records reuse the same super_interfaces and type_parameters nodes as classes, so the existing extractors apply unchanged. New extractRecordComponents converts formal_parameter components to fields by reusing extractParameter, so component annotations and doc comments come through exactly like method parameters. IsFinal stays unset even though components are implicitly final, because the resolver skips final fields (#141 covers that rule itself) — marking them would have dropped every record field again. The optional record body (compact constructors, methods, static members) is ignored rather than parsed, so it cannot cause parse failures
api-collector-java/parser/parser.go record_declaration joins class and interface in extractAllClasses
api-collector-java/testdata/CreateUserRequest.java Plain record fixture (the issue's acceptance example)
api-collector-java/testdata/PageResponseRecord.java Generic record fixture (PageResponseRecord<T>(List<T> items, int page, int total))
api-collector-java/testdata/ValidatedUserRecord.java Record with bean-validation annotations on components plus a compact constructor and a body method
api-collector-java/parser/parser_test.go TestParser_ParseRecord over the three fixtures: field names/types, package, type parameters, component annotations (@NotNull, @Size(min, max)), record JavaDoc, and proof that the compact constructor and body method do not become fields
api-collector-java/resolver/resolver_test.go TestResolve_SimpleRecord (record resolves to an object model with name/email/age) and TestResolve_GenericClassRecord (PageResponse<Order> binds T so items resolves to an array of Order objects with their own fields)
api-collector-java/springmvc/parser_test.go TestParser_RecordRequestBody: end-to-end over real source — a controller taking a record as @RequestBody exports a request body schema with name, email, and age

Records register in the class registry like any class, so generic records resolve through the existing type-parameter binding machinery with no resolver changes.

Verification

  • go test ./... passes for all 13 workspace modules, and go vet ./... is clean for api-collector-java.
  • Acceptance criteria from the issue:
    • public record CreateUserRequest(String name, String email, int age) as @RequestBody exports a request body with name, email, and age (TestParser_RecordRequestBody).
    • Existing class-based DTO tests still pass (full suite green, no golden files affected — this change is Java-collector-only and the CLI golden project is Go).

The Java parser only walked class_declaration and interface_declaration,
so a record used as a request or response DTO contributed zero fields:
record components are formal_parameter nodes under the declaration's
formal_parameters, not field_declaration nodes, and nothing looked for
record_declaration at all. A DTO written as the now-standard
`public record CreateUserRequest(String name, String email, int age)`
exported an empty request body.

Teach the parser records:

- extractor.go: extractRecord reads the name, annotations, type
  parameters, and implements clause off record_declaration (records
  reuse super_interfaces and type_parameters, so the existing
  extractors apply unchanged), and extractRecordComponents converts
  formal_parameter components to fields by reusing extractParameter, so
  component annotations and doc comments come through like method
  parameters. IsFinal stays unset even though components are implicitly
  final because the resolver skips final fields (#141 covers that rule
  itself) — marking them would have dropped every record field. The
  optional record body (compact constructors, methods, static members)
  is ignored rather than parsed.
- parser.go: record_declaration joins class and interface in
  extractAllClasses.

Records register in the class registry like any class, so generic
records resolve through the existing type-parameter binding:
PageResponse<T>(List<T> items, int total) requested as
PageResponse<Order> yields items as an array of Order objects.

Tests: fixtures for a plain record, a generic record, and a record with
bean-validation annotations plus a compact constructor; parser tests
over those fixtures; resolver tests for plain and generic record
resolution; and a springmvc test parsing real source where a record as
@RequestBody exports name, email, and age in the request body.
@github-actions

Copy link
Copy Markdown

📦 Build artifact for this PR is available in the GitHub Actions workflow run under Artifacts.

@tangcent
tangcent merged commit d676394 into main Sep 30, 2026
53 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] [Java] Resolve record components as DTO fields

1 participant