Skip to content

fix: avoid stack overflow errors when using heavily recursive types - #1377

Merged
jamietanna merged 12 commits into
oapi-codegen:mainfrom
yoshikipom:fix/recursive-error-allof
Apr 3, 2026
Merged

fix: avoid stack overflow errors when using heavily recursive types#1377
jamietanna merged 12 commits into
oapi-codegen:mainfrom
yoshikipom:fix/recursive-error-allof

Conversation

@yoshikipom

Copy link
Copy Markdown
Contributor

Issue

#1373
Stack overflow error happens merge_schemas.go when allOf contains circular $ref.

Cause & How to fix

https://github.com/deepmap/oapi-codegen/blob/1f53862bcc64573d3d0c4c105c71a8143e7b1816/pkg/codegen/merge_schemas.go#L87
mergeOpenapiSchemas is called recursively until all items in allOf are consumed. (allOf of item in allOf are also explored.) If same item is in the path of the exploration, stack overflow can happen.

https://github.com/deepmap/oapi-codegen/blob/1f53862bcc64573d3d0c4c105c71a8143e7b1816/pkg/codegen/merge_schemas.go#L85-L86
This code flatten schemas and merge them to handle allOf in allOf.

My approach is just skip to merge same schema because that doesn't affect to the merge result.

https://github.com/deepmap/oapi-codegen/compare/master...yoshikipom:oapi-codegen:fix/recursive-error-allof?expand=1#diff-423bfd1d22f4994f4f0c03f76f15af8371bee0bc5da50efa3ab696e1a60d7d49R41-R43

Commit

  • Add test e2ade03
    • I confirmed this test failed correctly before changes in next commit.
  • Fix stack overflow by skipping to merge same object c56f541

Test

Before fix

$ go test ./internal/test/issues/issue-1373/issue_test.go     
runtime: goroutine stack exceeds 1000000000-byte limit
runtime: sp=0x140203785d0 stack=[0x14020378000, 0x14040378000]
fatal error: stack overflow

runtime stack: ...

After fix

$ go test ./internal/test/issues/issue-1373/issue_test.go
ok      command-line-arguments  0.308s

@jamietanna
jamietanna requested a review from a team as a code owner September 20, 2024 11:26
@jamietanna jamietanna changed the title Fix stack overflow error in handling of allOf fix: avoid stack overflow errors when using heavily recursive types Sep 20, 2024
@jamietanna jamietanna added the bug Something isn't working label Sep 20, 2024
@jamietanna

Copy link
Copy Markdown
Member

Interestingly, this changes behaviour of an existing case:

 diff --git a/internal/test/all_of/v2/openapi.gen.go b/internal/test/all_of/v2/openapi.gen.go
index ab43034..618cec4 100644
--- a/internal/test/all_of/v2/openapi.gen.go
+++ b/internal/test/all_of/v2/openapi.gen.go
@@ -32,10 +32,10 @@ type PersonProperties struct {
 
 // PersonWithID defines model for PersonWithID.
 type PersonWithID struct {
-	FirstName          string `json:"FirstName"`
-	GovernmentIDNumber *int64 `json:"GovernmentIDNumber,omitempty"`
-	ID                 int64  `json:"ID"`
-	LastName           string `json:"LastName"`
+	FirstName          *string `json:"FirstName,omitempty"`
+	GovernmentIDNumber *int64  `json:"GovernmentIDNumber,omitempty"`
+	ID                 int64   `json:"ID"`
+	LastName           *string `json:"LastName,omitempty"`
 }

I need to confirm if that's expected

@jamietanna
jamietanna enabled auto-merge (squash) September 20, 2024 11:44
@jamietanna jamietanna modified the milestones: v2.4.0, v2.5.0 Sep 20, 2024
@jamietanna jamietanna modified the milestones: v2.5.0, v2.6.0 Jul 15, 2025
# Conflicts:
#	.github/workflows/generate.yml
@mromaszewicz

Copy link
Copy Markdown
Member

@greptileai

…e schemas

Inline schemas have an empty Ref string. Without this guard, the
seenSchemaRef map would key on empty string, causing all inline
schemas after the first to be incorrectly skipped during allOf merging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Apr 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a stack overflow in mergeOpenapiSchemas caused by circular $ref in allOf by threading a seenSchemaRef map through the recursive merge functions to skip already-visited schema refs.

  • P1 — incomplete cycle detection: allOf[0]'s ref is never seeded into seenSchemaRef. When the recursive self-ref is the first entry in allOf, mergeAllOf(s1.AllOf, seenSchemaRef) is called without it in the map, and the stack overflow still occurs for those specs.
  • P2 — test spec typo: FieldInRecursive:: in spec.yaml (double colon) creates a property named FieldInRecursive: with a trailing colon in the JSON tag.

Confidence Score: 3/5

Not safe to merge as-is — the fix only handles the case where the self-ref is not the first allOf entry; placing the recursive ref first still stack overflows.

The core approach (tracking visited refs to break cycles) is correct, but the implementation has a coverage gap: allOf[0]'s ref is never added to the seen-set, leaving a variant of the original bug unfixed for specs where the self-reference appears as the first allOf item.

pkg/codegen/merge_schemas.go — specifically the seenSchemaRef initialization in mergeSchemas before and during the loop.

Important Files Changed

Filename Overview
pkg/codegen/merge_schemas.go Core fix: seenSchemaRef map passed through recursive mergeAllOf/mergeOpenapiSchemas to break cycles, but allOf[0]'s ref is never seeded into the map, leaving a stack-overflow path when the self-ref is first in allOf.
pkg/codegen/merge_schemas_test.go Existing tests updated to pass the new seenSchemaRef argument to mergeOpenapiSchemas; no new unit tests for the cycle-detection logic itself.
internal/test/issues/issue-1373/spec.yaml New test fixture for the recursive-allOf scenario; contains a likely typo (FieldInRecursive::) that gives the property a colon in its name.
internal/test/issues/issue-1373/config.yaml Minimal codegen config for the new issue-1373 test; no issues.
internal/test/issues/issue-1373/generate.go Standard go:generate directive for the issue-1373 test package; no issues.

Reviews (2): Last reviewed commit: "fix: only track named refs in cycle dete..." | Re-trigger Greptile

Comment thread pkg/codegen/merge_schemas.go Outdated
Comment thread pkg/codegen/merge_schemas.go
@mromaszewicz

Copy link
Copy Markdown
Member

@greptileai

@jamietanna
jamietanna merged commit a76544b into oapi-codegen:main Apr 3, 2026
18 checks passed
Comment on lines +42 to +46
seenSchemaRef := make(map[string]bool)
if allOf[i].Ref != "" {
seenSchemaRef[allOf[i].Ref] = true
}
schema, err = mergeOpenapiSchemas(schema, oneOfSchema, true, seenSchemaRef)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 allOf[0] self-ref not seeded into seenSchemaRef

allOf[0]'s ref is never added to any seenSchemaRef map before the loop. Inside mergeOpenapiSchemas, s1 is derived from allOf[0].Value, and if s1.AllOf contains a ref back to the same schema (i.e., the recursive self-ref is placed first in allOf), mergeAllOf(s1.AllOf, seenSchemaRef) won't detect the cycle — the ref isn't in the map — and will stack overflow again.

Concretely, a spec like the following would still crash:

RecursiveObject:
  allOf:
    - $ref: "#/components/schemas/RecursiveObject"   # self-ref is allOf[0]
    - $ref: "#/components/schemas/NonRecursiveObject"

Seeding allOf[0].Ref into an initial map before iterating would close this gap:

seenTopLevel := make(map[string]bool)
if allOf[0].Ref != "" {
    seenTopLevel[allOf[0].Ref] = true
}
for i := 1; i < n; i++ {
    seenSchemaRef := make(map[string]bool)
    for k, v := range seenTopLevel { seenSchemaRef[k] = v }
    if allOf[i].Ref != "" {
        seenSchemaRef[allOf[i].Ref] = true
        seenTopLevel[allOf[i].Ref] = true
    }
    ...
}

mromaszewicz added a commit to mromaszewicz/oapi-codegen that referenced this pull request Apr 4, 2026
PR oapi-codegen#1377 was merged too early. This is a small follow-up tweak to make
sure that oapi-codegen#1373 is fixed properly.

seed allOf[0] ref into cycle detection to prevent stack overflow when self-ref is first

When the self-referencing $ref appears at position 0 in allOf, its
ref was never added to seenSchemaRef. If s1's own AllOf contained a
back-reference to itself, mergeAllOf would not detect the cycle.

Seed allOf[0].Ref into a top-level seen set that is copied into each
iteration's seenSchemaRef map, closing this gap.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
mromaszewicz added a commit that referenced this pull request Apr 11, 2026
PR #1377 was merged too early. This is a small follow-up tweak to make
sure that #1373 is fixed properly.

seed allOf[0] ref into cycle detection to prevent stack overflow when self-ref is first

When the self-referencing $ref appears at position 0 in allOf, its
ref was never added to seenSchemaRef. If s1's own AllOf contained a
back-reference to itself, mergeAllOf would not detect the cycle.

Seed allOf[0].Ref into a top-level seen set that is copied into each
iteration's seenSchemaRef map, closing this gap.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.

3 participants