[object Object]

← back to Cli Printing Press

fix(cli): cap body-flag recursion depth to prevent compiler OOM (#1522)

82ad787b8a38591e611fdc968630cda45a27e28f · 2026-05-16 12:29:12 -0700 · Trevin Chow

* fix(cli): cap body-flag recursion depth to prevent compiler OOM

Deeply nested OpenAPI request bodies (Tripletex, NetSuite, SAP S/4HANA, and
similar enterprise specs) produced POST/PUT command files with 40k+ lines as
the body-flag emitters recursed through every nested object. The Go compiler
ran out of memory (signal: killed) before finishing the package.

Caps recursion at maxBodyFlagDepth = 3 across the four body-emitter helpers:
renderBodyMap, renderBodyVarDecls, renderBodyFlagRegs, renderBodyRequiredChecks.
When the next depth would meet the cap, the object's subtree is skipped
uniformly across all four; deeper fields are reachable via the existing
--stdin flag on POST/PUT/PATCH commands.

The --stdin flag's help text is rewritten when truncation fires so an
operator inspecting --help sees the affordance without reading the issue
tracker. Behavior for shallow specs (<= 2 levels of nesting) is unchanged
across all 20 golden fixtures.

Closes #1240
Refs #1520

* fix(cli): walk flattened body for depth-cap truncation signal

bodyExceedsFlagDepth read endpoint.Body directly, but the four emitters
walk flattenCollidingBodyFields(endpoint.Body). When dot-flattened
identifier collision clears an object's Fields, the emitters treat that
subtree as a JSON-string leaf and never recurse past it, so no depth-cap
truncation actually occurs. The predicate, however, still saw the
original deep structure and returned true, rewriting the --stdin help
text to the truncation-warning variant even though every field was
exposed as a per-field flag.

Walk flattenCollidingBodyFields in the predicate so detection and
emission cannot drift. Adds a regression test for the
collision-flattened deep subtree case.

Refs #1240

---------

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>

Files touched

Diff

commit 82ad787b8a38591e611fdc968630cda45a27e28f
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Sat May 16 12:29:12 2026 -0700

    fix(cli): cap body-flag recursion depth to prevent compiler OOM (#1522)
    
    * fix(cli): cap body-flag recursion depth to prevent compiler OOM
    
    Deeply nested OpenAPI request bodies (Tripletex, NetSuite, SAP S/4HANA, and
    similar enterprise specs) produced POST/PUT command files with 40k+ lines as
    the body-flag emitters recursed through every nested object. The Go compiler
    ran out of memory (signal: killed) before finishing the package.
    
    Caps recursion at maxBodyFlagDepth = 3 across the four body-emitter helpers:
    renderBodyMap, renderBodyVarDecls, renderBodyFlagRegs, renderBodyRequiredChecks.
    When the next depth would meet the cap, the object's subtree is skipped
    uniformly across all four; deeper fields are reachable via the existing
    --stdin flag on POST/PUT/PATCH commands.
    
    The --stdin flag's help text is rewritten when truncation fires so an
    operator inspecting --help sees the affordance without reading the issue
    tracker. Behavior for shallow specs (<= 2 levels of nesting) is unchanged
    across all 20 golden fixtures.
    
    Closes #1240
    Refs #1520
    
    * fix(cli): walk flattened body for depth-cap truncation signal
    
    bodyExceedsFlagDepth read endpoint.Body directly, but the four emitters
    walk flattenCollidingBodyFields(endpoint.Body). When dot-flattened
    identifier collision clears an object's Fields, the emitters treat that
    subtree as a JSON-string leaf and never recurse past it, so no depth-cap
    truncation actually occurs. The predicate, however, still saw the
    original deep structure and returned true, rewriting the --stdin help
    text to the truncation-warning variant even though every field was
    exposed as a per-field flag.
    
    Walk flattenCollidingBodyFields in the predicate so detection and
    emission cannot drift. Adds a regression test for the
    collision-flattened deep subtree case.
    
    Refs #1240
    
    ---------
    
    Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
---
 internal/generator/body_collision_test.go          | 115 ++++++++--
 internal/generator/body_map_test.go                | 237 +++++++++++++++++++++
 internal/generator/generator.go                    |  93 ++++++--
 .../generator/templates/command_endpoint.go.tmpl   |   4 +
 4 files changed, 421 insertions(+), 28 deletions(-)

diff --git a/internal/generator/body_collision_test.go b/internal/generator/body_collision_test.go
index 404bf80d..7f9efcb3 100644
--- a/internal/generator/body_collision_test.go
+++ b/internal/generator/body_collision_test.go
@@ -6,6 +6,7 @@ import (
 	"go/token"
 	"os"
 	"path/filepath"
+	"strconv"
 	"strings"
 	"testing"
 
@@ -262,6 +263,13 @@ func TestGenerateDeduplicatesNestedTreesCollapsedToSameIdent(t *testing.T) {
 // unique identifiers (bodyProjectCustomerName vs
 // bodyProjectPostalAddressCustomerName) without falling back on the _N
 // suffix.
+//
+// maxBodyFlagDepth truncates the deeper
+// Project.PostalAddress.Customer.name leaf (depth 3); only the shallower
+// Project.Customer.name (depth 2) emits. The dedup behavior is still
+// exercised: were the cap raised, the parent-prefix walker would
+// uniquify both leaves without collision. Truncation is verified via
+// the deeper identifier's absence and the --stdin help text rewrite.
 func TestGenerateDeduplicatesConvergentNestedBodyPaths(t *testing.T) {
 	t.Parallel()
 
@@ -304,12 +312,12 @@ func TestGenerateDeduplicatesConvergentNestedBodyPaths(t *testing.T) {
 		"convergent nested paths must produce distinct Go identifiers")
 	assertNoDuplicates(t, flagBindings,
 		"convergent nested paths must register distinct cobra flag names")
-	require.Len(t, bodyVars, 2,
-		"both convergent name leaves must survive as distinct Go identifiers")
+	require.Len(t, bodyVars, 1,
+		"only the shallower project.customer.name survives the depth cap")
 	assert.Contains(t, bodyVars, "bodyProjectCustomerName",
-		"the direct project.customer.name leaf produces this identifier")
-	assert.Contains(t, bodyVars, "bodyProjectPostalAddressCustomerName",
-		"the project.postalAddress.customer.name leaf produces this identifier via path-prefix joining")
+		"the depth-2 project.customer.name leaf still emits its identifier")
+	assert.NotContains(t, bodyVars, "bodyProjectPostalAddressCustomerName",
+		"the depth-3 project.postalAddress.customer.name leaf is truncated by the cap")
 }
 
 // TestGenerateDeduplicatesCyclicRefBodyShape drives the full OpenAPI
@@ -420,17 +428,94 @@ components:
 	assertNoDuplicates(t, flagBindings,
 		"cyclic-ref body shape must register distinct cobra flag names")
 
-	// The cycle-cut shape produces three leaves: the direct
-	// project.customer.name string and two cycle-cut customer objects
-	// at distinct paths.
-	require.Len(t, bodyVars, 3,
-		"every direct and cycle-cut leaf must survive dedup as a distinct Go identifier")
+	// maxBodyFlagDepth truncates the cycle-cut leaves at depth 3+; only
+	// the direct project.customer.name leaf at depth 2 survives as a
+	// per-field flag. The dedup pass remains the line of defense for any
+	// identifier collisions inside the surviving depth window. Deeper
+	// leaves are reachable via --stdin (the template's help text is
+	// rewritten to advertise the fallback when truncation fires; see
+	// bodyExceedsFlagDepth).
+	require.Len(t, bodyVars, 1,
+		"only the shallow project.customer.name survives the depth cap")
 	assert.Contains(t, bodyVars, "bodyProjectCustomerName",
-		"the direct project.customer.name leaf must emit bodyProjectCustomerName")
-	assert.Contains(t, bodyVars, "bodyProjectCustomerLedgerAccountVatTypeCustomer",
-		"the ledgerAccount.vatType.customer cycle-cut leaf must emit a unique identifier under its full path")
-	assert.Contains(t, bodyVars, "bodyProjectCustomerPostalAddressCustomer",
-		"the postalAddress.customer cycle-cut leaf must emit a unique identifier under its full path")
+		"the direct depth-2 project.customer.name leaf still emits bodyProjectCustomerName")
+	assert.NotContains(t, bodyVars, "bodyProjectCustomerLedgerAccountVatTypeCustomer",
+		"the depth-5 ledgerAccount.vatType.customer cycle-cut leaf is truncated by the cap")
+	assert.NotContains(t, bodyVars, "bodyProjectCustomerPostalAddressCustomer",
+		"the depth-4 postalAddress.customer cycle-cut leaf is truncated by the cap")
+
+	// The template's --stdin help text must be rewritten to advertise
+	// the fallback when truncation fires, so an operator inspecting
+	// `--help` sees the affordance without needing to read the issue
+	// tracker.
+	src, err := os.ReadFile(postFile)
+	require.NoError(t, err)
+	assert.Contains(t, string(src),
+		`"Read request body as JSON from stdin (use this for deeply nested fields not exposed as flags)"`,
+		"--stdin help text must reflect truncation when bodyExceedsFlagDepth is true")
+}
+
+// TestGenerateBodyDepthCapPreventsCompilerExplosion guards the
+// regression a spec whose request body recurses past the depth cap
+// must not balloon the emitted Go file. Without the cap, deeply
+// recursive bodies produce 40k+ line files that OOM-kill the Go
+// compiler.
+func TestGenerateBodyDepthCapPreventsCompilerExplosion(t *testing.T) {
+	t.Parallel()
+
+	apiSpec := minimalSpec("deep-body-cap")
+	// Build a body with reasonable fan-out (4 fields per level) nested
+	// 6 levels deep. Without the cap, this produces 4^6 = 4096 leaves
+	// and the corresponding flag/var/map blocks. With cap=3, only
+	// 4 + 4 + 4 + 4 = 16 leaves at depths 0..2 plus the depth-2
+	// objects that recurse one level deeper but skip the recursion.
+	build := func(depth int) []spec.Param {
+		var current []spec.Param
+		current = []spec.Param{
+			{Name: "field0", Type: "string"},
+			{Name: "field1", Type: "string"},
+			{Name: "field2", Type: "string"},
+			{Name: "field3", Type: "string"},
+		}
+		for i := depth - 1; i >= 0; i-- {
+			nested := append([]spec.Param{}, current...)
+			current = []spec.Param{
+				{Name: "field0", Type: "string"},
+				{Name: "field1", Type: "string"},
+				{Name: "field2", Type: "string"},
+				{Name: "level" + strconv.Itoa(i), Type: "object", Fields: nested},
+			}
+		}
+		return current
+	}
+
+	apiSpec.Resources["payloads"] = spec.Resource{
+		Description: "Deep payloads",
+		Endpoints: map[string]spec.Endpoint{
+			"create": {
+				Method:      "POST",
+				Path:        "/payloads",
+				Description: "Submit a deeply nested payload",
+				Body:        build(6),
+			},
+			"get": {Method: "GET", Path: "/payloads/{id}", Description: "Get one"},
+		},
+	}
+
+	outputDir := filepath.Join(t.TempDir(), "deep-body-cap-pp-cli")
+	require.NoError(t, New(apiSpec, outputDir).Generate())
+
+	postFile := filepath.Join(outputDir, "internal", "cli", "payloads_create.go")
+
+	// Exact-count assertion catches off-by-one cap regressions that a
+	// file-size threshold would miss. Fixture has 3 scalar siblings at
+	// each of depths 0, 1, 2 = 9 body-var declarations under cap=3.
+	// Cap=2 would drop to 6; cap=4 would jump to 12; no cap at all
+	// produces 4 + (3 leaves * 5 inner levels) + 4 = 23 leaves down the
+	// single deep chain, and far more in the general fan-out shape.
+	bodyVars, _ := parseBodyDeclarations(t, postFile)
+	require.Len(t, bodyVars, 9,
+		"cap=3 must produce exactly 9 body-var declarations (3 leaves at each of depths 0, 1, 2)")
 }
 
 // TestFlattenCollidingBodyFields_NestedPrefixShape covers the Atlassian
diff --git a/internal/generator/body_map_test.go b/internal/generator/body_map_test.go
index 68ff8da1..7407b496 100644
--- a/internal/generator/body_map_test.go
+++ b/internal/generator/body_map_test.go
@@ -1,6 +1,7 @@
 package generator
 
 import (
+	"strconv"
 	"strings"
 	"testing"
 
@@ -611,3 +612,239 @@ func TestBodyJSONFallback_RequiredChecks_RequiredBody(t *testing.T) {
 		t.Errorf("expected body-json in error message, got:%q", got)
 	}
 }
+
+// deepBodyFixture builds a body with one root object whose Fields chain
+// `levels` deep, ending in a string leaf. Each interior object has a
+// scalar sibling so the truncation test can verify which depths emit and
+// which are dropped.
+//
+//	body[level0Obj] (depth 0) ->
+//	  level0Obj.sibling0 (string, depth 1 leaf)
+//	  level0Obj.level1Obj (depth 1 object) ->
+//	    level1Obj.sibling1 (string, depth 2 leaf)
+//	    level1Obj.level2Obj (depth 2 object) ->
+//	      level2Obj.sibling2 (string, depth 3 leaf, truncated at cap=3)
+//	      level2Obj.level3Obj (depth 3 object, truncated at cap=3) -> ...
+func deepBodyFixture(levels int) []spec.Param {
+	if levels < 1 {
+		return nil
+	}
+	// Build from the innermost leaf outward.
+	current := []spec.Param{{Name: "leaf", Type: "string"}}
+	for i := levels - 1; i >= 0; i-- {
+		fields := append([]spec.Param{}, current...)
+		fields = append([]spec.Param{{
+			Name: "sibling" + strconv.Itoa(i),
+			Type: "string",
+		}}, fields...)
+		current = []spec.Param{{
+			Name:   "level" + strconv.Itoa(i) + "Obj",
+			Type:   "object",
+			Fields: fields,
+		}}
+	}
+	return current
+}
+
+// TestBodyMap_DepthCap_TruncatesBelowMax verifies that body-map
+// emission stops recursing into nested objects at maxBodyFlagDepth. A
+// fixture nested 6 levels deep must produce per-field assignments only
+// for depths 0..maxBodyFlagDepth-1; deeper subtrees are silently
+// omitted (the user reaches them via --stdin).
+func TestBodyMap_DepthCap_TruncatesBelowMax(t *testing.T) {
+	t.Parallel()
+	got := bodyMap(deepBodyFixture(6), "\t")
+
+	// The depth-2 sibling and the depth-2 nested map block should both
+	// appear (the cap allows three levels: 0, 1, 2).
+	if !strings.Contains(got, "bodyLevel0ObjLevel1ObjSibling1") {
+		t.Errorf("expected depth-2 sibling leaf to emit, got:\n%s", got)
+	}
+	if !strings.Contains(got, "nestedLevel0ObjLevel1Obj") {
+		t.Errorf("expected depth-2 nested map block, got:\n%s", got)
+	}
+	// The depth-3 sibling, depth-3 object block, and any deeper identifiers
+	// must be absent.
+	if strings.Contains(got, "Sibling2") {
+		t.Errorf("depth-3 sibling must be truncated by the cap, got:\n%s", got)
+	}
+	if strings.Contains(got, "nestedLevel0ObjLevel1ObjLevel2Obj") {
+		t.Errorf("depth-3 nested map block must be truncated, got:\n%s", got)
+	}
+	if strings.Contains(got, "Level3Obj") || strings.Contains(got, "Leaf") {
+		t.Errorf("anything below depth-2 must be omitted, got:\n%s", got)
+	}
+}
+
+// TestBodyVarDecls_DepthCap pins the var-declaration set for a deep body.
+// Only fields reachable within the cap should produce `var bodyX` lines.
+func TestBodyVarDecls_DepthCap(t *testing.T) {
+	t.Parallel()
+	got := bodyVarDecls(spec.Endpoint{Body: deepBodyFixture(6)})
+	for _, want := range []string{
+		"\n\tvar bodyLevel0ObjSibling0 string",
+		"\n\tvar bodyLevel0ObjLevel1ObjSibling1 string",
+	} {
+		if !strings.Contains(got, want) {
+			t.Errorf("expected within-cap var decl %q, got:\n%s", want, got)
+		}
+	}
+	for _, banned := range []string{
+		"bodyLevel0ObjLevel1ObjLevel2ObjSibling2",
+		"bodyLevel0ObjLevel1ObjLevel2ObjLevel3Obj",
+	} {
+		if strings.Contains(got, banned) {
+			t.Errorf("depth-capped identifier %q must not emit, got:\n%s", banned, got)
+		}
+	}
+}
+
+// TestBodyFlagRegs_DepthCap pins cobra flag registrations for a deep
+// body: only flags reachable within the cap are registered.
+func TestBodyFlagRegs_DepthCap(t *testing.T) {
+	t.Parallel()
+	got := bodyFlagRegs(spec.Endpoint{Body: deepBodyFixture(6)})
+	if !strings.Contains(got, `"level0-obj-level1-obj-sibling1"`) {
+		t.Errorf("expected depth-2 flag registration, got:\n%s", got)
+	}
+	if strings.Contains(got, "sibling2") {
+		t.Errorf("depth-3 flag must be truncated, got:\n%s", got)
+	}
+}
+
+// TestBodyMap_DepthCap_ShallowUnchanged ensures specs that fit inside
+// the cap (2 levels of nesting) emit identical output to today: no
+// truncation, every leaf reachable.
+func TestBodyMap_DepthCap_ShallowUnchanged(t *testing.T) {
+	t.Parallel()
+	got := bodyMap(deepBodyFixture(2), "\t")
+	for _, want := range []string{
+		"bodyLevel0ObjSibling0",
+		"bodyLevel0ObjLevel1ObjSibling1",
+		"bodyLevel0ObjLevel1ObjLeaf",
+	} {
+		if !strings.Contains(got, want) {
+			t.Errorf("2-level fixture must emit %q with no truncation, got:\n%s", want, got)
+		}
+	}
+}
+
+// TestBodyExceedsFlagDepth_True reports truncation when the body nests
+// past the cap. A 4-level fixture exceeds maxBodyFlagDepth=3.
+func TestBodyExceedsFlagDepth_True(t *testing.T) {
+	t.Parallel()
+	if !bodyExceedsFlagDepth(spec.Endpoint{Body: deepBodyFixture(4)}) {
+		t.Error("4-level body must report truncation under cap=3")
+	}
+}
+
+// TestBodyExceedsFlagDepth_False reports no truncation when the body
+// fits inside the cap.
+func TestBodyExceedsFlagDepth_False(t *testing.T) {
+	t.Parallel()
+	if bodyExceedsFlagDepth(spec.Endpoint{Body: deepBodyFixture(2)}) {
+		t.Error("2-level body must not report truncation under cap=3")
+	}
+	if bodyExceedsFlagDepth(spec.Endpoint{Body: []spec.Param{{Name: "n", Type: "string"}}}) {
+		t.Error("flat body must not report truncation")
+	}
+}
+
+// TestBodyExceedsFlagDepth_BodyJSONFallback never reports truncation
+// for oneOf/anyOf bodies; those route through a single --body-json flag
+// and never reach the per-field emitter.
+func TestBodyExceedsFlagDepth_BodyJSONFallback(t *testing.T) {
+	t.Parallel()
+	if bodyExceedsFlagDepth(spec.Endpoint{BodyJSONFallback: true, Body: deepBodyFixture(6)}) {
+		t.Error("BodyJSONFallback bypasses per-field emission and must not report truncation")
+	}
+}
+
+// TestBodyExceedsFlagDepth_CollisionFlattenedSubtree pins the
+// interaction with flattenCollidingBodyFields. When dot-flattened
+// identifier collision clears an object's Fields, the emitters treat
+// that subtree as a JSON-string leaf and never recurse past it -- no
+// depth-cap truncation occurs. The predicate must read the same
+// flattened tree the emitters render, or the --stdin help text reverts
+// to the truncation-warning variant when every field is in fact a
+// per-field flag.
+func TestBodyExceedsFlagDepth_CollisionFlattenedSubtree(t *testing.T) {
+	t.Parallel()
+	// Top-level scalar 'outerInnerLeaf' collides with the dot-flattened
+	// nested leaf outer.inner.leaf (both camelize to bodyOuterInnerLeaf
+	// at depth 3). flattenCollidingBodyFields clears outer.Fields so the
+	// emitters render outer as a JSON-string leaf at depth 0; no part of
+	// the rendered tree exceeds the cap.
+	body := []spec.Param{
+		{Name: "outerInnerLeaf", Type: "string"},
+		{Name: "outer", Type: "object", Fields: []spec.Param{
+			{Name: "inner", Type: "object", Fields: []spec.Param{
+				{Name: "leaf", Type: "string"},
+			}},
+		}},
+	}
+	if bodyExceedsFlagDepth(spec.Endpoint{Body: body}) {
+		t.Error("collision-flattened subtree must not report truncation; emitters see a flat tree")
+	}
+}
+
+// TestBodyExceedsFlagDepth_Multipart returns false for multipart
+// endpoints regardless of nested-object depth: bodyUsesFlatEmission
+// keeps multipart and form-encoded bodies one-flag-per-top-level-param,
+// so deep nesting never triggers the per-field recursion the cap guards.
+func TestBodyExceedsFlagDepth_Multipart(t *testing.T) {
+	t.Parallel()
+	endpoint := spec.Endpoint{
+		Method:             "POST",
+		RequestContentType: "multipart/form-data",
+		Body:               deepBodyFixture(6),
+	}
+	if bodyExceedsFlagDepth(endpoint) {
+		t.Error("multipart endpoint must not report truncation; flat emission never recurses")
+	}
+}
+
+// TestBodyMap_DepthCap_Boundary pins the exact depth at which the cap
+// fires. A fixture nested exactly maxBodyFlagDepth levels is the
+// minimal spec that triggers truncation: the depth-2 object's children
+// are at depth 3 and the recursion check (depth+1 >= maxBodyFlagDepth)
+// stops the walk. The depth-1 sibling must still emit; the depth-2
+// sibling and deeper leaves must be absent. A regression that toggled
+// the check to `>` vs `>=` would flip this assertion.
+func TestBodyMap_DepthCap_Boundary(t *testing.T) {
+	t.Parallel()
+	got := bodyMap(deepBodyFixture(maxBodyFlagDepth), "\t")
+	if !strings.Contains(got, "bodyLevel0ObjSibling0") {
+		t.Errorf("depth-1 sibling must emit at the boundary fixture, got:\n%s", got)
+	}
+	if !strings.Contains(got, "bodyLevel0ObjLevel1ObjSibling1") {
+		t.Errorf("depth-2 sibling must emit at the boundary fixture, got:\n%s", got)
+	}
+	if strings.Contains(got, "Sibling2") || strings.Contains(got, "Leaf") {
+		t.Errorf("boundary fixture must not emit depth-3 leaves, got:\n%s", got)
+	}
+}
+
+// TestBodyRequiredChecks_DepthCap omits required-flag checks for fields
+// truncated by the cap. The --stdin path bypasses required checks
+// wholesale, so deep required fields are reachable via stdin only.
+func TestBodyRequiredChecks_DepthCap(t *testing.T) {
+	t.Parallel()
+	// Build a deep fixture where the deepest sibling is required.
+	body := deepBodyFixture(6)
+	// Walk to the depth-3 sibling and mark it required.
+	cursor := &body[0]
+	for range 3 {
+		// cursor.Fields[0] is "siblingN" (string leaf), cursor.Fields[1] is
+		// "level<N+1>Obj" (the nested object). Descend the object branch.
+		cursor = &cursor.Fields[1]
+	}
+	// cursor now points at level3Obj; mark its sibling (depth 4 leaf)
+	// required.
+	cursor.Fields[0].Required = true
+
+	got := bodyRequiredChecks(spec.Endpoint{Body: body}, "\t\t\t")
+	if strings.Contains(got, "sibling3") {
+		t.Errorf("required check on a truncated deep leaf must not emit, got:\n%s", got)
+	}
+}
diff --git a/internal/generator/generator.go b/internal/generator/generator.go
index 7db4349d..47f03fbf 100644
--- a/internal/generator/generator.go
+++ b/internal/generator/generator.go
@@ -316,6 +316,7 @@ func New(s *spec.APISpec, outputDir string) *Generator {
 		"bodyVarDecls":          bodyVarDecls,
 		"bodyFlagRegs":          bodyFlagRegs,
 		"bodyRequiredChecks":    bodyRequiredChecks,
+		"bodyExceedsFlagDepth":  bodyExceedsFlagDepth,
 		"multipartBodyMaps":     multipartBodyMaps,
 		"endpointUsesMultipart": endpointUsesMultipart,
 		"endpointHasQueryFlags": endpointHasQueryFlags,
@@ -3383,6 +3384,21 @@ func endpointNeedsClientLimit(endpoint spec.Endpoint) bool {
 	return false
 }
 
+// maxBodyFlagDepth caps how many levels of nested-object recursion the
+// body-flag emitters expand into per-field Cobra flags. A Param at
+// depth 0 is a top-level body field; its object children are at depth 1
+// and recurse with depth+1. When the next depth would meet or exceed
+// the cap, the object's subtree is skipped uniformly across renderBodyMap,
+// renderBodyVarDecls, renderBodyFlagRegs, and renderBodyRequiredChecks.
+// The user reaches truncated fields via the existing `--stdin` flag on
+// POST/PUT/PATCH commands, which reads the full JSON body from stdin.
+//
+// Default 3 covers typical CRUD schemas (resource.object.field) without
+// the recursive explosion seen on enterprise/ERP specs that self-reference
+// through 6+ layers and produce 40k-line command files the Go compiler
+// OOMs on.
+const maxBodyFlagDepth = 3
+
 // bodyMap renders the per-flag body-building block shared by the
 // POST/PUT/PATCH branches in command_endpoint.go.tmpl and the body
 // branch in command_promoted.go.tmpl. The four sites generated the
@@ -3395,10 +3411,11 @@ func endpointNeedsClientLimit(endpoint spec.Endpoint) bool {
 // recurses: each leaf field becomes its own flag (parent-prefixed in
 // the generated identifier so `start.dateTime` and `end.dateTime` do
 // not collide), and the parent's wire-side key receives a built-up
-// map[string]any rather than a single JSON-string flag.
+// map[string]any rather than a single JSON-string flag. Recursion stops
+// at maxBodyFlagDepth; deeper subtrees are only reachable via `--stdin`.
 func bodyMap(body []spec.Param, indent string) string {
 	var b strings.Builder
-	renderBodyMap(&b, flattenCollidingBodyFields(body), indent, "body", "", "")
+	renderBodyMap(&b, flattenCollidingBodyFields(body), 0, indent, "body", "", "")
 	return b.String()
 }
 
@@ -3439,16 +3456,19 @@ func bodyJSONFallbackMap(indent string) string {
 	return b.String()
 }
 
-func renderBodyMap(b *strings.Builder, body []spec.Param, indent, mapVar, identPrefix, flagPrefix string) {
+func renderBodyMap(b *strings.Builder, body []spec.Param, depth int, indent, mapVar, identPrefix, flagPrefix string) {
 	for _, p := range body {
 		id := paramIdent(p)
 		ident := identPrefix + toCamel(id)
 		flag := joinFlag(flagPrefix, publicFlagName(p))
 		if p.Type == "object" && len(p.Fields) > 0 {
+			if depth+1 >= maxBodyFlagDepth {
+				continue
+			}
 			nestedMap := "nested" + ident
 			fmt.Fprintf(b, "%s{\n", indent)
 			fmt.Fprintf(b, "%s\t%s := map[string]any{}\n", indent, nestedMap)
-			renderBodyMap(b, p.Fields, indent+"\t", nestedMap, ident, flag)
+			renderBodyMap(b, p.Fields, depth+1, indent+"\t", nestedMap, ident, flag)
 			fmt.Fprintf(b, "%s\tif len(%s) > 0 {\n", indent, nestedMap)
 			fmt.Fprintf(b, "%s\t\t%s[%q] = %s\n", indent, mapVar, p.Name, nestedMap)
 			fmt.Fprintf(b, "%s\t}\n", indent)
@@ -3518,7 +3538,7 @@ func bodyVarDecls(endpoint spec.Endpoint) string {
 		}
 		return b.String()
 	}
-	renderBodyVarDecls(&b, flattenCollidingBodyFields(endpoint.Body), "")
+	renderBodyVarDecls(&b, flattenCollidingBodyFields(endpoint.Body), 0, "")
 	return b.String()
 }
 
@@ -3531,11 +3551,14 @@ func bodyUsesFlatEmission(endpoint spec.Endpoint) bool {
 	return endpointUsesMultipart(endpoint) || endpointUsesForm(endpoint)
 }
 
-func renderBodyVarDecls(b *strings.Builder, body []spec.Param, identPrefix string) {
+func renderBodyVarDecls(b *strings.Builder, body []spec.Param, depth int, identPrefix string) {
 	for _, p := range body {
 		ident := identPrefix + toCamel(paramIdent(p))
 		if p.Type == "object" && len(p.Fields) > 0 {
-			renderBodyVarDecls(b, p.Fields, ident)
+			if depth+1 >= maxBodyFlagDepth {
+				continue
+			}
+			renderBodyVarDecls(b, p.Fields, depth+1, ident)
 			continue
 		}
 		fmt.Fprintf(b, "\n\tvar body%s %s", ident, goType(p.Type))
@@ -3560,16 +3583,19 @@ func bodyFlagRegs(endpoint spec.Endpoint) string {
 		}
 		return b.String()
 	}
-	renderBodyFlagRegs(&b, flattenCollidingBodyFields(endpoint.Body), "", "", true)
+	renderBodyFlagRegs(&b, flattenCollidingBodyFields(endpoint.Body), 0, "", "", true)
 	return b.String()
 }
 
-func renderBodyFlagRegs(b *strings.Builder, body []spec.Param, identPrefix, flagPrefix string, topLevel bool) {
+func renderBodyFlagRegs(b *strings.Builder, body []spec.Param, depth int, identPrefix, flagPrefix string, topLevel bool) {
 	for _, p := range body {
 		if p.Type == "object" && len(p.Fields) > 0 {
+			if depth+1 >= maxBodyFlagDepth {
+				continue
+			}
 			ident := identPrefix + toCamel(paramIdent(p))
 			flag := joinFlag(flagPrefix, publicFlagName(p))
-			renderBodyFlagRegs(b, p.Fields, ident, flag, false)
+			renderBodyFlagRegs(b, p.Fields, depth+1, ident, flag, false)
 			continue
 		}
 		renderFlatBodyFlagReg(b, p, identPrefix, flagPrefix, topLevel)
@@ -3622,21 +3648,62 @@ func bodyRequiredChecks(endpoint spec.Endpoint, indent string) string {
 		}
 		return b.String()
 	}
-	renderBodyRequiredChecks(&b, flattenCollidingBodyFields(endpoint.Body), indent, "", true)
+	renderBodyRequiredChecks(&b, flattenCollidingBodyFields(endpoint.Body), 0, indent, "", true)
 	return b.String()
 }
 
-func renderBodyRequiredChecks(b *strings.Builder, body []spec.Param, indent, flagPrefix string, topLevel bool) {
+func renderBodyRequiredChecks(b *strings.Builder, body []spec.Param, depth int, indent, flagPrefix string, topLevel bool) {
 	for _, p := range body {
 		if p.Type == "object" && len(p.Fields) > 0 {
+			if depth+1 >= maxBodyFlagDepth {
+				continue
+			}
 			flag := joinFlag(flagPrefix, publicFlagName(p))
-			renderBodyRequiredChecks(b, p.Fields, indent, flag, false)
+			renderBodyRequiredChecks(b, p.Fields, depth+1, indent, flag, false)
 			continue
 		}
 		renderFlatBodyRequiredCheck(b, p, indent, flagPrefix, topLevel)
 	}
 }
 
+// bodyExceedsFlagDepth reports whether emitting per-field body flags for
+// the endpoint would have truncated any nested-object subtree under
+// maxBodyFlagDepth. Multipart/form endpoints stay flat and never
+// truncate; BodyJSONFallback endpoints route through a single
+// --body-json flag and never reach the per-field path.
+//
+// The walk uses flattenCollidingBodyFields because that is what the
+// emitters render. Collision-flattening clears `Fields` on an object
+// whose dot-flattened subtree would clash with a sibling identifier,
+// turning it into a JSON-string leaf the user passes as a single flag.
+// Walking the raw body would falsely report truncation in that case
+// and rewrite the --stdin help text even when every field is exposed.
+func bodyExceedsFlagDepth(endpoint spec.Endpoint) bool {
+	if endpoint.BodyJSONFallback || bodyUsesFlatEmission(endpoint) {
+		return false
+	}
+	return walkBodyExceedsDepth(flattenCollidingBodyFields(endpoint.Body), 0)
+}
+
+// walkBodyExceedsDepth returns true as soon as any nested-object subtree
+// at depth >= maxBodyFlagDepth-1 is found. The walk is bounded by the
+// same depth check the emitters use, so a Param graph that
+// intentionally self-references (cyclic spec) does not loop here.
+func walkBodyExceedsDepth(body []spec.Param, depth int) bool {
+	for _, p := range body {
+		if p.Type != "object" || len(p.Fields) == 0 {
+			continue
+		}
+		if depth+1 >= maxBodyFlagDepth {
+			return true
+		}
+		if walkBodyExceedsDepth(p.Fields, depth+1) {
+			return true
+		}
+	}
+	return false
+}
+
 func renderFlatBodyRequiredCheck(b *strings.Builder, p spec.Param, indent, flagPrefix string, topLevel bool) {
 	if !p.Required || p.Default != nil {
 		return
diff --git a/internal/generator/templates/command_endpoint.go.tmpl b/internal/generator/templates/command_endpoint.go.tmpl
index ab24938b..8ed96435 100644
--- a/internal/generator/templates/command_endpoint.go.tmpl
+++ b/internal/generator/templates/command_endpoint.go.tmpl
@@ -650,8 +650,12 @@ func new{{camel .FuncPrefix}}{{camel .EndpointName}}Cmd(flags *rootFlags) *cobra
 	cmd.Flags().BoolVar(&flagAll, "all", false, "Fetch all pages")
 {{- end}}
 {{- if and (or (eq .Endpoint.Method "POST") (eq .Endpoint.Method "PUT") (eq .Endpoint.Method "PATCH")) (not $isMultipart) (not $isForm)}}
+{{- if bodyExceedsFlagDepth .Endpoint}}
+	cmd.Flags().BoolVar(&stdinBody, "stdin", false, "Read request body as JSON from stdin (use this for deeply nested fields not exposed as flags)")
+{{- else}}
 	cmd.Flags().BoolVar(&stdinBody, "stdin", false, "Read request body as JSON from stdin")
 {{- end}}
+{{- end}}
 {{- if .IsAsync}}
 	cmd.Flags().BoolVar(&flagWait, "wait", false, "Block until the submitted job reaches a terminal status")
 	cmd.Flags().DurationVar(&flagWaitTimeout, "wait-timeout", 10*time.Minute, "Maximum duration to wait when --wait is set (0 = no timeout)")

← 0c3c8bcc fix(cli): detect integer page paginator and advance numerica  ·  back to Cli Printing Press  ·  fix(skills): require codex completion marker before treating 99637941 →