Skip to content

fix(goctl): resolve multi-level struct embedding in TypeScript generation#5407

Open
kevwan wants to merge 3 commits intozeromicro:masterfrom
kevwan:fix/multi-level-struct-embedding
Open

fix(goctl): resolve multi-level struct embedding in TypeScript generation#5407
kevwan wants to merge 3 commits intozeromicro:masterfrom
kevwan:fix/multi-level-struct-embedding

Conversation

@kevwan
Copy link
Copy Markdown
Contributor

@kevwan kevwan commented Feb 6, 2026

Description

Fixes #5402

When using multi-level nesting in type definitions (e.g., AgeStruct embeds NameStruct which embeds IdStruct), the TypeScript code generation was losing fields from the deepest embedded structs.

Problem

Given the following API definition:

type (
    IdStruct {
        Id int64 `json:"id"`
    }
    NameStruct {
        IdStruct
        Name string `json:"name"`
    }
    AgeStruct {
        NameStruct
        Age int `json:"age"`
    }
)

The generated TypeScript was incorrectly missing the id field:

export interface AgeStruct {
    name: string
    age: number
}

Root Cause

The type resolution in fillTypes() did a single pass through types, looking up embedded struct references from the original unresolved type list. This meant multi-level embeddings couldn't resolve properly because nested embedded types were not yet fully resolved when they were referenced.

Solution

Implemented iterative type resolution that:

  1. Resolves types in multiple passes until no changes occur
  2. Looks up types from both the currently-resolved types (within the iteration) and the original list
  3. Prioritizes recently-resolved types to handle multi-level embeddings correctly

Result

After the fix, the generated TypeScript correctly includes all fields:

export interface AgeStruct {
    id: number      // ✅ Now included (from IdStruct)
    name: string    // ✅ From NameStruct
    age: number     // ✅ Direct field
}

Testing

  • Verified fix with the reported test case
  • Existing parser tests continue to pass

This ensures all embedded struct fields are properly flattened in the generated TypeScript interfaces.

Copilot AI review requested due to automatic review settings February 6, 2026 13:54
@codecov
Copy link
Copy Markdown

codecov Bot commented Feb 6, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the API parser’s type-resolution step so that embedded structs are resolved across multiple levels, preventing TypeScript generation from losing fields coming from deeply embedded structs (as reported in #5402).

Changes:

  • Reworks Analyzer.fillTypes() to iteratively resolve embedded spec.DefineStruct references until resolution stabilizes.
  • Adds a helper findTypeInList() to prefer types already resolved earlier in the same iteration.

Comment on lines +371 to +376
// Check if the type changed (was resolved further)
if resolvedType, ok := tp.(spec.DefineStruct); ok {
if len(resolvedType.Members) != len(v.Members) {
changed = true
}
}
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

The loop break condition can stop iterations too early: changed is only set when the embedded struct’s member count changes. For deeper embedding chains (e.g., A embeds B embeds C embeds D), B/C can become “more resolved” (inner embedded members replaced) without changing len(Members), so changed stays false and earlier structs may never pick up the newly-resolved versions. Consider setting changed when the resolved type differs from the current member.Type (e.g., !reflect.DeepEqual(member.Type, tp)), or alternatively always run up to maxIterations and break only when no member.Type replacements occur in an iteration.

Copilot uses AI. Check for mistakes.
Comment on lines +346 to +349
// Resolve embedded struct types iteratively until all references are fully resolved.
// This handles multi-level struct embedding where a struct embeds another struct
// that itself embeds yet another struct.
maxIterations := len(a.spec.Types)
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

This change is a regression fix for multi-level struct embedding resolution, but there isn’t an automated test covering it in this package. Adding a parser/analyzer test that asserts embedded fields are fully flattened (including a chain deeper than 2 levels and with types defined out-of-order) would prevent future regressions.

Copilot generated this review using guidance from repository custom instructions.
…tion

Fixes zeromicro#5402

When using multi-level nesting in type definitions (e.g., AgeStruct embeds
NameStruct which embeds IdStruct), the TypeScript code generation was losing
fields from the deepest embedded structs.

Root cause: The type resolution in fillTypes() did a single pass through
types, looking up embedded struct references from the original unresolved
type list. This meant multi-level embeddings couldn't resolve properly.

Solution: Implement iterative type resolution that:
1. Resolves types in multiple passes until no changes occur
2. Looks up types from both the currently-resolved types and original list
3. Prioritizes recently-resolved types to handle multi-level embeddings

This ensures all embedded struct fields are properly flattened in the
generated TypeScript interfaces.
Add comprehensive test cases to verify that multi-level struct embedding
is correctly resolved by the parser:

1. Test 2-level embedding (AgeStruct -> NameStruct -> IdStruct)
2. Test 3-level embedding (FullProfile -> ExtendedProfile ->
   UserProfile -> BaseUser)

The tests verify that embedded struct references are fully resolved with
all their members, ensuring the fix in analyzer.go works correctly for
multi-level type resolution.
@kevwan kevwan force-pushed the fix/multi-level-struct-embedding branch from f70818f to d8fed9d Compare February 15, 2026 08:23
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.

Multi-level nested type definitions in API lose hierarchy information when generating TypeScript files

2 participants