diff --git a/.changesets/1790922409-830b3652.yaml b/.changesets/1790922409-830b3652.yaml
new file mode 100644
index 00000000..e4cef780
--- /dev/null
+++ b/.changesets/1790922409-830b3652.yaml
@@ -0,0 +1,10 @@
+id: 1790922409-830b3652
+features:
+ - examples
+targets:
+ - cli
+type: fix
+bump: patch
+description: preserve CLI examples for empty and optional request bodies, omit redundant empty JSON/form body flags, and retain required body validation
+author: TristanSpeakEasy
+date: "2026-10-02"
diff --git a/pkg/generate/snapshots/cli_empty_body_go_test.go b/pkg/generate/snapshots/cli_empty_body_go_test.go
new file mode 100644
index 00000000..f6cf0e22
--- /dev/null
+++ b/pkg/generate/snapshots/cli_empty_body_go_test.go
@@ -0,0 +1,269 @@
+package snapshots
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "regexp"
+ "testing"
+
+ "github.com/speakeasy-api/openapi-generation/v2/pkg/generate/snapshots/snaptest"
+ "github.com/stretchr/testify/require"
+)
+
+const cliEmptyBodySpec = `openapi: 3.0.3
+info:
+ title: Widget API
+ version: 1.0.0
+servers:
+ - url: https://api.example.com
+x-test-operation: &mixedOperation
+ description: An optional empty object body accompanies a required path parameter.
+ tags: [widgets]
+ security: []
+ parameters:
+ - name: id
+ in: path
+ required: true
+ example: widget_123
+ schema:
+ type: string
+ requestBody: &emptyBody
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/EmptyBody'
+ responses: &responses
+ '200':
+ description: Accepted
+paths:
+ /widgets/{id}/optional:
+ post:
+ <<: *mixedOperation
+ operationId: optional
+ /widgets/{id}/optionalexample:
+ post:
+ <<: *mixedOperation
+ operationId: optionalexample
+ description: An explicit empty object example does not require an optional body.
+ requestBody:
+ content:
+ application/json:
+ example: {}
+ schema:
+ $ref: '#/components/schemas/EmptyBody'
+ /widgets/{id}/required:
+ post:
+ <<: *mixedOperation
+ operationId: required
+ description: A required empty object body accepts an empty object without invented properties.
+ requestBody:
+ <<: *emptyBody
+ required: true
+ /widgets/standaloneoptional:
+ post:
+ <<: *mixedOperation
+ operationId: standaloneoptional
+ description: An optional empty object body is the only operation input.
+ parameters: []
+ /widgets/standalonerequired:
+ post:
+ <<: *mixedOperation
+ operationId: standalonerequired
+ description: A required empty object body is the only operation input.
+ parameters: []
+ requestBody:
+ <<: *emptyBody
+ required: true
+ /widgets/{id}/formoptional:
+ post:
+ <<: *mixedOperation
+ operationId: formoptional
+ description: An optional empty form object accompanies a required path parameter.
+ requestBody: &emptyFormBody
+ content:
+ application/x-www-form-urlencoded:
+ schema:
+ $ref: '#/components/schemas/EmptyBody'
+ /widgets/{id}/formrequired:
+ post:
+ <<: *mixedOperation
+ operationId: formrequired
+ description: A required empty form object accepts an empty object without invented properties.
+ requestBody:
+ <<: *emptyFormBody
+ required: true
+ /widgets/{id}/scalaroptional:
+ post:
+ <<: *mixedOperation
+ operationId: scalaroptional
+ description: An optional scalar body without an object example does not need an invented object payload.
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: string
+ /widgets/{id}/nullablerequired:
+ post:
+ <<: *mixedOperation
+ operationId: nullablerequired
+ description: A nullable empty object body does not need an invented payload.
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ nullable: true
+ properties: {}
+ /widgets/{id}/nullableoptional:
+ post:
+ <<: *mixedOperation
+ operationId: nullableoptional
+ description: An optional nullable empty object body preserves the distinction between omission and null.
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ nullable: true
+ properties: {}
+ /widgets/standalonenullable:
+ post:
+ <<: *mixedOperation
+ operationId: standalonenullable
+ description: A nullable empty object body is the only operation input.
+ parameters: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ nullable: true
+ properties: {}
+ /widgets/{id}/maprequired:
+ post:
+ <<: *mixedOperation
+ operationId: maprequired
+ description: A map body retains its whole-body input flag.
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ additionalProperties:
+ type: string
+ /widgets/{id}/unionrequired:
+ post:
+ <<: *mixedOperation
+ operationId: unionrequired
+ description: A union body retains its whole-body input flag and explicit example.
+ requestBody:
+ required: true
+ content:
+ application/json:
+ example:
+ label: sample
+ schema:
+ oneOf:
+ - type: object
+ required: [label]
+ properties:
+ label:
+ type: string
+ - type: string
+ /widgets/parameter:
+ get:
+ operationId: parameter
+ description: An empty object query parameter remains a JSON input rather than a body wrapper.
+ tags: [widgets]
+ security: []
+ parameters:
+ - name: filter
+ in: query
+ schema:
+ $ref: '#/components/schemas/EmptyBody'
+ responses: *responses
+components:
+ schemas:
+ EmptyBody:
+ description: An object with no declared properties.
+ type: object
+ properties: {}
+`
+
+func TestSnapCLIEmptyBodyExamplesAndFlags(t *testing.T) {
+ for _, style := range []string{"compact", "full"} {
+ t.Run(style, func(t *testing.T) {
+ shouldCompile := false
+ snaptest.DoTestSnapshot(t, snaptest.Options{
+ Spec: cliEmptyBodySpec,
+ GenYaml: fmt.Sprintf(`cli:
+ packageName: github.com/example/widget-cli
+ cliName: widget-cli
+ envVarPrefix: WIDGET
+ helpStyle: %s
+`, style),
+ ShouldCompile: &shouldCompile,
+ AfterGenerate: func(t *testing.T, outputDir string) {
+ t.Helper()
+ readCommand := func(name string) string {
+ t.Helper()
+ content, err := os.ReadFile(filepath.Join(outputDir, "internal", "cli", "widgets", name+".go"))
+ require.NoError(t, err)
+ return string(content)
+ }
+ examplePattern := regexp.MustCompile(`Example:\s*"([^"\n]*)"`)
+ for _, name := range []string{"optional", "optionalexample", "scalaroptional", "formoptional", "nullablerequired"} {
+ command := readCommand(name)
+ require.Regexp(t, examplePattern, command)
+ require.Equal(t, " widget-cli widgets "+name+" --id widget_123", examplePattern.FindStringSubmatch(command)[1])
+ }
+ for _, name := range []string{"optional", "optionalexample", "required", "formoptional", "formrequired"} {
+ command := readCommand(name)
+ require.Contains(t, command, `cmd.Flags().String("body",`)
+ require.NotContains(t, command, `FlagName: "body-param"`)
+ }
+ usageSource, err := os.ReadFile(filepath.Join(outputDir, "internal", "usage", "schema.go"))
+ require.NoError(t, err)
+ usagePattern := regexp.MustCompile(`"widgets (optional|optionalexample|required|formoptional|formrequired)":\s*"(.*)"`)
+ usageSchemas := usagePattern.FindAllStringSubmatch(string(usageSource), -1)
+ require.Len(t, usageSchemas, 5)
+ for _, schema := range usageSchemas {
+ require.Contains(t, schema[2], "--body
")
+ require.NotContains(t, schema[2], "--body-param")
+ }
+ for _, name := range []string{"required", "formrequired"} {
+ command := readCommand(name)
+ require.Regexp(t, examplePattern, command)
+ require.Equal(t, " widget-cli widgets "+name+" --id widget_123 --body '{}'", examplePattern.FindStringSubmatch(command)[1])
+ require.Contains(t, command, `PromptFlagSpec{Required: true, Kind: "json", BodyFlag: true}`)
+ }
+ standaloneOptional := readCommand("standaloneoptional")
+ require.Regexp(t, examplePattern, standaloneOptional)
+ require.Equal(t, " widget-cli widgets standaloneoptional", examplePattern.FindStringSubmatch(standaloneOptional)[1])
+ standaloneNullable := readCommand("standalonenullable")
+ require.Regexp(t, examplePattern, standaloneNullable)
+ require.Equal(t, " widget-cli widgets standalonenullable", examplePattern.FindStringSubmatch(standaloneNullable)[1])
+ require.NotContains(t, readCommand("optional"), "alternative to individual flags")
+ nullableOptional := readCommand("nullableoptional")
+ require.Contains(t, nullableOptional, "Kind: flagutil.FlagKindJSON")
+ require.Contains(t, nullableOptional, "alternative to individual flags")
+ standaloneRequired := readCommand("standalonerequired")
+ require.Contains(t, standaloneRequired, "--empty-body '{}'")
+ for _, command := range []string{standaloneOptional, standaloneRequired} {
+ require.Contains(t, command, `cmd.Flags().String("empty-body",`)
+ require.Contains(t, command, "flagutil.BuildRequestBody[")
+ }
+ require.Contains(t, readCommand("parameter"), `FlagName: "filter"`)
+ require.Contains(t, readCommand("parameter"), "Kind: flagutil.FlagKindJSON")
+ require.Contains(t, readCommand("maprequired"), `FlagName: "body-param"`)
+ require.Contains(t, readCommand("unionrequired"), "Kind: flagutil.FlagKindUnion")
+ require.Contains(t, readCommand("unionrequired"), `--body '{\"label\":\"sample\"}'`)
+ },
+ })
+ })
+ }
+}
diff --git a/templates/templates/cli/README.md b/templates/templates/cli/README.md
index 83a791f4..99fa3d06 100644
--- a/templates/templates/cli/README.md
+++ b/templates/templates/cli/README.md
@@ -1248,6 +1248,8 @@ Declared commands may add a nested `help:` map with the closed keys `defaults`,
Compact operation examples suppress only generator-synthesized invocations containing its own angle-bracket value placeholders or fallback `{"key": "value"}` body. Spec examples, manifest examples, and hand-written `Example` text are never filtered. An intent with no authored example gets a bare invocation only when it has no unresolved required positional or declared flag.
+In both help styles, `includes/descriptions.ts` omits invented payloads for optional or nullable, unexpanded request bodies without usable body examples, preserving examples for required parameters. Empty JSON/form request-body classes use `'{}'` when required rather than an object with invented properties. Mixed parameter/body operations with an empty JSON/form body register only the existing `--body` flag for that body, not a redundant `--body-param` metadata flag, unless the body uses a nullable/optional wrapper. Wrapped bodies retain their individual JSON flag and the `--body` help describes that alternative. Required empty bodies mark `--body` as required for prompting, and `BuildRequest` rejects omission or JSON `null` while accepting `'{}'` from the flag or stdin. The metadata, whole-body flag lookup, and `--usage` generators share the same field filter (`includes/metadata.ts`, `includes/usage.ts`). Body-only operations retain their whole-body flag, named from the type for shared/component models or from the request field for operation-specific models. Empty non-body classes, maps, unions, and nullable/optional wrappers retain their existing input flags.
+
The compact layout and `Just works:` heading are runtime help contracts. Cobra markdown generation keeps its existing `Examples` heading and operation-doc footer, and the explorer continues to read `Short`, `Long`, and `Example` without a heading rewrite. Because synthesized-operation filtering happens while `templateCmdExample` builds the command's `Example`, a rejected synthesized example is absent from docs and the explorer too; authored examples remain unchanged everywhere. Custom commands that currently append Defaults/Machine/Globals prose to `Example` should migrate that prose to the annotations above to avoid a duplicate footer, set `speakeasy_help_footer: "false"` while retaining their own footer, or choose `helpStyle: full` during migration.
The KDL schema is built at generation time from the command tree in `includes/usage.ts` and emitted at runtime by `usage.EmitSchema()`. It includes command names, aliases, help text, flags, defaults, config metadata, and the config file location. Generated flags include the same auto-assigned short forms as the parser and `--help` (for example, `flag "-i --id "` when `--id` receives `-i`). Shorthand assignment still reserves inherited global letters and the pagination `-a`; route-dispatch intents expose only shorthands for the non-body flags they register.
diff --git a/templates/templates/cli/auxiliary/internal/flagutil/metadata.go.stmpl b/templates/templates/cli/auxiliary/internal/flagutil/metadata.go.stmpl
index 1cfc9221..205cc5d1 100644
--- a/templates/templates/cli/auxiliary/internal/flagutil/metadata.go.stmpl
+++ b/templates/templates/cli/auxiliary/internal/flagutil/metadata.go.stmpl
@@ -662,8 +662,16 @@ func BuildRequest[T any](cmd *cobra.Command, meta []FlagMeta, bodyFieldPath stri
v := reflect.ValueOf(&req).Elem()
bodyPrePopulated := false
hasRequestBody := bodyFieldPath != "" || bodyFlagName != "" || !isJSONSerialized(v.Type())
+ bodyRequired := false
+ if flag := cmd.Flags().Lookup(bodyFlagName); flag != nil {
+ values := flag.Annotations[AnnotationRequired]
+ bodyRequired = len(values) > 0 && values[0] == "true"
+ }
decodeBody := func(data []byte, source string) error {
+ if bodyRequired && bytes.Equal(bytes.TrimSpace(data), []byte("null")) {
+ return WithCLIValidation(fmt.Errorf("invalid value for --%s: null; the body is required", bodyFlagName))
+ }
u := bodyUnionMeta(meta, bodyFieldPath)
if bodyFieldPath != "" {
bodyField, err := navigateToField(v, bodyFieldPath)
@@ -723,6 +731,10 @@ func BuildRequest[T any](cmd *cobra.Command, meta []FlagMeta, bodyFieldPath stri
}
}
+ if bodyRequired && !bodyPrePopulated {
+ return nil, &MissingRequiredFlagError{FlagName: bodyFlagName, Detail: "(or provide via stdin)"}
+ }
+
// When body provided via --body flag or stdin, relax Required checks for body fields
// so builders don't error for fields already populated
if bodyPrePopulated {
diff --git a/templates/templates/cli/includes/descriptions.ts b/templates/templates/cli/includes/descriptions.ts
index 21e1f6e7..ed7a37c4 100644
--- a/templates/templates/cli/includes/descriptions.ts
+++ b/templates/templates/cli/includes/descriptions.ts
@@ -438,8 +438,12 @@ function templateCmdExample(op: Operation): string {
Value: `--${flagName} '${JSON.stringify(ex.Value)}'`,
SynthesizedPlaceholder: ex.Synthesized,
}));
- } else {
- pushPart(`--${flagName} '{"key": "value"}'`, true);
+ } else if (!bodyField.Optional && !bodyField.Nullable) {
+ if (isEmptyRequestBodyClass(bodyField)) {
+ pushPart(`--${flagName} '{}'`);
+ } else {
+ pushPart(`--${flagName} '{"key": "value"}'`, true);
+ }
}
}
} else if (op.Request.RequestBody) {
@@ -478,8 +482,12 @@ function templateCmdExample(op: Operation): string {
Value: `--${flagName} '${JSON.stringify(ex.Value)}'`,
SynthesizedPlaceholder: ex.Synthesized,
}));
- } else {
- pushPart(`--${flagName} '{"key": "value"}'`, true);
+ } else if (!bodyField.Optional && !bodyField.Nullable) {
+ if (isEmptyRequestBodyClass(bodyField)) {
+ pushPart(`--${flagName} '{}'`);
+ } else {
+ pushPart(`--${flagName} '{"key": "value"}'`, true);
+ }
}
} else if (bodyField && isMultipartMixedOp(op)) {
// Multipart bodies have no whole-body JSON flag. Keep examples runnable
diff --git a/templates/templates/cli/includes/metadata.ts b/templates/templates/cli/includes/metadata.ts
index 45ed1c70..5a9d9ffa 100644
--- a/templates/templates/cli/includes/metadata.ts
+++ b/templates/templates/cli/includes/metadata.ts
@@ -48,6 +48,15 @@ function operationHasFlagMetadata(op: Operation): boolean {
}
registerTemplateFunc("operationHasFlagMetadata", operationHasFlagMetadata);
+function getMixedRequestMetadataFields(op: Operation): FieldDef[] {
+ const fields = op.Request.Field.Type.Fields || [];
+ if (!templateHasBodyFlag(op)) return fields;
+ return fields.filter(
+ (field: FieldDef) =>
+ !isEmptyRequestBodyClass(field) || isNullableOptionalWrapped(field),
+ );
+}
+
// buildOperationMetadataEntries constructs the FlagMeta entry strings for an
// operation — the single source for both the emitted metadata var and any
// validation that must mirror it (e.g. auto-shorthand collision checks).
@@ -76,7 +85,7 @@ function buildOperationMetadataEntries(
return null; // Complex JSON IsRequestBody uses BuildRequestBody, no metadata var
}
} else {
- const fields = op.Request.Field.Type.Fields;
+ const fields = getMixedRequestMetadataFields(op);
hadCandidateFields = fields.some((f: FieldDef) => !f.Const);
collectMetadataFromFields(fields, "", "", entries);
}
@@ -774,12 +783,7 @@ function wholeBodyFlagName(op: Operation): string {
const bodyFieldPath = getBodyFieldPath(op);
if (!bodyFieldPath) return "";
const entries: string[] = [];
- collectMetadataFromFields(
- op.Request.Field.Type.Fields || [],
- "",
- "",
- entries,
- );
+ collectMetadataFromFields(getMixedRequestMetadataFields(op), "", "", entries);
// Entries are Go literals that always open with the top-level FlagName and
// FieldPath (buildMetaEntryForField / the union entry); match that prefix so
// nested variant fields cannot be mistaken for the body field.
@@ -967,12 +971,28 @@ function templateHasBodyFlag(op: Operation): boolean {
}
registerTemplateFunc("templateHasBodyFlag", templateHasBodyFlag);
+function templateEmptyBodyRequired(op: Operation): boolean {
+ if (!op.Request || op.Request.IsRequestBody || !templateHasBodyFlag(op)) {
+ return false;
+ }
+ return (op.Request.Field.Type.Fields || []).some(
+ (field: FieldDef) =>
+ isEmptyRequestBodyClass(field) && !field.Optional && !field.Nullable,
+ );
+}
+registerTemplateFunc("templateEmptyBodyRequired", templateEmptyBodyRequired);
+
/**
* Generate the description for the --body flag.
*/
function templateBodyFlagDescription(op: Operation): string {
- let description =
- "Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF.";
+ const hasEmptyBody =
+ op.Request?.RequestBody &&
+ isEmptyRequestBodyClass(op.Request.RequestBody) &&
+ !isNullableOptionalWrapped(op.Request.RequestBody);
+ let description = hasEmptyBody
+ ? "Request body as JSON. Can also be provided via stdin; @path reads a file, @- reads stdin to EOF."
+ : "Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF.";
if (hasBodySchemaForOp(op)) {
description += " Use --schema to print the exact JSON Schema.";
}
diff --git a/templates/templates/cli/includes/test-workflows.ts b/templates/templates/cli/includes/test-workflows.ts
index e0178262..0c607c3e 100644
--- a/templates/templates/cli/includes/test-workflows.ts
+++ b/templates/templates/cli/includes/test-workflows.ts
@@ -264,7 +264,10 @@ function templateCLIStepCode(usageContext: UsageContext): string {
if (operation.Request.RequestBody) {
bodyHandled = true;
const reqBodyField = operation.Request.RequestBody;
- const flagName = getRequestBodyFlagName(reqBodyField);
+ const flagName =
+ !operation.Request.IsRequestBody && templateHasBodyFlag(operation)
+ ? "body"
+ : getRequestBodyFlagName(reqBodyField);
const bodyIsExpandable = shouldExpandNestedField(reqBodyField);
const example = findExampleWithFallback(
operation.Request.Examples ?? [],
diff --git a/templates/templates/cli/includes/tests.ts b/templates/templates/cli/includes/tests.ts
index e04941d5..71e1e71c 100644
--- a/templates/templates/cli/includes/tests.ts
+++ b/templates/templates/cli/includes/tests.ts
@@ -1132,6 +1132,28 @@ function templateCLIArgs(usageContext: UsageContext): string {
continue;
}
+ if (
+ isEmptyRequestBodyClass(field) &&
+ templateHasBodyFlag(operation) &&
+ !isNullableOptionalWrapped(field)
+ ) {
+ if (
+ bodyExamplePayload !== undefined ||
+ (!field.Optional && !field.Nullable)
+ ) {
+ pushFlagArg(
+ args,
+ "body",
+ goStringLiteral(
+ JSON.stringify(
+ bodyExamplePayload === undefined ? {} : bodyExamplePayload,
+ ),
+ ),
+ );
+ }
+ continue;
+ }
+
// Check if this is a body struct field that gets expanded into nested flags
// (no param annotation + class type + expanded by CLI command via shouldExpandNestedField)
// Also handle multipart body fields which are expanded via collectMultipartMetadata
diff --git a/templates/templates/cli/includes/usage.ts b/templates/templates/cli/includes/usage.ts
index 50ddb1af..7ae3c8e1 100644
--- a/templates/templates/cli/includes/usage.ts
+++ b/templates/templates/cli/includes/usage.ts
@@ -500,7 +500,7 @@ function getOperationBodyFieldUsageFlags(op: Operation): UsageFlagDef[] {
walkFields(op.Request.RequestBody.Type.Fields || [], "");
}
} else {
- walkFields(op.Request.Field.Type.Fields || [], "");
+ walkFields(getMixedRequestMetadataFields(op), "");
}
return withAutoShorthands(op, flags, false);
diff --git a/templates/templates/cli/includes/utils.ts b/templates/templates/cli/includes/utils.ts
index 40afe16c..882bcee9 100644
--- a/templates/templates/cli/includes/utils.ts
+++ b/templates/templates/cli/includes/utils.ts
@@ -57,6 +57,21 @@ function getInputClassType(field: FieldDef): InputClassType {
}
registerTemplateFunc("getInputClassType", getInputClassType);
+function isEmptyRequestBodyClass(field: FieldDef): boolean {
+ if (
+ field.Type.Type.toString() !== "class" ||
+ (field.Type.Fields || []).length !== 0 ||
+ !field.Annotations?.Has("request")
+ ) {
+ return false;
+ }
+ const requestAnno = field.Annotations.Get("request") as RequestAnnotation;
+ return (
+ matchContentType(requestAnno.MediaType, "application/json") ||
+ matchContentType(requestAnno.MediaType, "application/x-www-form-urlencoded")
+ );
+}
+
function isNullableOptionalWrapped(field: FieldDef): boolean {
return Boolean(
field.Nullable &&
diff --git a/templates/templates/cli/opcmd.go.stmpl b/templates/templates/cli/opcmd.go.stmpl
index 7cee08b9..eb42fdec 100644
--- a/templates/templates/cli/opcmd.go.stmpl
+++ b/templates/templates/cli/opcmd.go.stmpl
@@ -92,7 +92,7 @@ func {{$initFuncName}}(parent *cobra.Command) error {
{{- if templateHasBodyFlag $op}}
cmd.Flags().String("body", "", "{{templateBodyFlagDescription $op}}")
{{- addImport "fmt"}}
- _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true})
+ _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Required: {{templateEmptyBodyRequired $op}}, Kind: "json", BodyFlag: true})
cmd.Annotations[flagutil.AnnotationWholeBodyFlag] = "body"
if err := flagutil.AnnotateBodyFields(cmd, {{templateMetaVarName $op}}, "{{templateBodyFieldPath $op}}", "body"); err != nil {
return fmt.Errorf("annotate body fields for {{sanitizeCLICommand $op.GetID}}: %w", err)
diff --git a/templates/templates/cli/tests/primary/bodyinput_test.go.stmpl b/templates/templates/cli/tests/primary/bodyinput_test.go.stmpl
index 9e3f1071..2528f078 100644
--- a/templates/templates/cli/tests/primary/bodyinput_test.go.stmpl
+++ b/templates/templates/cli/tests/primary/bodyinput_test.go.stmpl
@@ -3,9 +3,11 @@ package tests
import (
"os"
"path/filepath"
+ "strings"
"testing"
"time"
+ "github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -13,6 +15,45 @@ import (
"{{getGolangPackage}}/internal/output"
)
+func TestRequiredEmptyMixedBody(t *testing.T) {
+ type request struct {
+ Body struct{} `json:"body"`
+ }
+ for _, tc := range []struct {
+ name string
+ body string
+ setBody bool
+ stdin string
+ required bool
+ wantError bool
+ }{
+ {name: "required omitted", required: true, wantError: true},
+ {name: "required flag", body: "{}", setBody: true, required: true},
+ {name: "required stdin", stdin: "{}", required: true},
+ {name: "required empty flag", setBody: true, required: true, wantError: true},
+ {name: "required null flag", body: "null", setBody: true, required: true, wantError: true},
+ {name: "required null stdin", stdin: "null", required: true, wantError: true},
+ {name: "optional omitted"},
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ cmd := &cobra.Command{}
+ cmd.SetIn(strings.NewReader(tc.stdin))
+ cmd.Flags().String("body", "", "Request body as JSON")
+ require.NoError(t, flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Required: tc.required, Kind: "json", BodyFlag: true}))
+ if tc.setBody {
+ require.NoError(t, cmd.Flags().Set("body", tc.body))
+ }
+ _, err := flagutil.BuildRequest[request](cmd, nil, "Body", "body")
+ if tc.wantError {
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "--body")
+ } else {
+ require.NoError(t, err)
+ }
+ })
+ }
+}
+
// withSilentStdinPipe swaps os.Stdin for the read end of a pipe whose write
// end stays open and never produces data — the shape agent runtimes and exec
// wrappers hand a CLI — and restores it when the test ends.
diff --git a/zSDKs/sdk-cli/.speakeasy/gen.lock b/zSDKs/sdk-cli/.speakeasy/gen.lock
index 363a844e..bb5f18dc 100644
--- a/zSDKs/sdk-cli/.speakeasy/gen.lock
+++ b/zSDKs/sdk-cli/.speakeasy/gen.lock
@@ -62,15 +62,15 @@ trackedFiles:
internal/cli/auth.go:
last_write_checksum: sha1:6eb9d73f31d4922ceeea044a1d92e46821cc04eb
internal/cli/binaryandstringupload.go:
- last_write_checksum: sha1:c47f65053b62472a62de6a8f0de0cc6ce28d63f1
+ last_write_checksum: sha1:20d8028b7618e73dcf1763372f9145ea325b32b0
internal/cli/chat.go:
last_write_checksum: sha1:3d1fa09fa2658d3c8800cecafa30f677180a327f
internal/cli/configure.go:
last_write_checksum: sha1:ad89cfd7fd30f6395fc006ee54d3f2e640160c21
internal/cli/createuser.go:
- last_write_checksum: sha1:dfd3a094a5e4e9f70d9aa051b3edee96a36c799f
+ last_write_checksum: sha1:5b60000059db85dcf4c90f5e67c37b7fff3cf852
internal/cli/createwithunion.go:
- last_write_checksum: sha1:65128825f54a5666104684573d4e257300f7d14e
+ last_write_checksum: sha1:b0f2c0ce986fe2bee70d002373e9e1e78be84671
internal/cli/deleteuser.go:
last_write_checksum: sha1:e6876685816745a290da3903a03a3afe8f0812e7
internal/cli/getasset.go:
@@ -86,7 +86,7 @@ trackedFiles:
internal/cli/geterroronlyexample.go:
last_write_checksum: sha1:31437c3769f79c492aa0e7a7f173a9f73413c102
internal/cli/getfullyflattenedrequest.go:
- last_write_checksum: sha1:d8a6bbf230b9cd2e96720d5cf44ee6a7c44bd0b3
+ last_write_checksum: sha1:81f559c8f866ccf39e63d0a6ce592993188a6246
internal/cli/getnamedprimitiveunion.go:
last_write_checksum: sha1:663d361d6a46c5d729350af9afe88d4ee9b266e3
internal/cli/getnestedintegerstring.go:
@@ -136,7 +136,7 @@ trackedFiles:
internal/cli/masking.go:
last_write_checksum: sha1:5a69fa055d628c1b8507cd123f3cd18309aa9a53
internal/cli/namespacetests/conflicts/createnamespaceconflict.go:
- last_write_checksum: sha1:f0b084215afa94936b2cb5cbfe50a8d32e44b07a
+ last_write_checksum: sha1:1ef390ab92c1aa4416b370734d02f567eaa9fdd1
internal/cli/namespacetests/conflicts/getnamespaceconflict.go:
last_write_checksum: sha1:8730dacebc73d9b364b77196c2ff47e7c84d36af
internal/cli/namespacetests/conflicts/getpetowners.go:
@@ -144,7 +144,7 @@ trackedFiles:
internal/cli/namespacetests/conflicts/gettriplenamespaceconflict.go:
last_write_checksum: sha1:6e70cde7b555f23e0f22b6b1281794ab6ba6eb5a
internal/cli/namespacetests/conflicts/putnamespaceconflict.go:
- last_write_checksum: sha1:51e7b258d5587d45cffa6d99a9836ab1589513a8
+ last_write_checksum: sha1:4a200a397b2f268275eb3db62bf6fbce70fe66df
internal/cli/namespacetests/conflicts/root.go:
last_write_checksum: sha1:02a213614562bf56b1d4655435039c5932c35732
internal/cli/namespacetests/root.go:
@@ -154,7 +154,7 @@ trackedFiles:
internal/cli/namespacetests/singlebar/root.go:
last_write_checksum: sha1:9588b75e31fc6974aa5c46bf6ab37200a37b2c8b
internal/cli/namespacetests/singlefoo/createsinglenamespacefoopet.go:
- last_write_checksum: sha1:f1c2a663ddf1e5f6354149ceb2df3489055fcb36
+ last_write_checksum: sha1:cd178ef84ed126fa22d0099ae51d74a6e717dd0c
internal/cli/namespacetests/singlefoo/getsinglenamespacefoopet.go:
last_write_checksum: sha1:e042198f9fa2f8105c8c39824edcc823bbc5e305
internal/cli/namespacetests/singlefoo/root.go:
@@ -176,11 +176,11 @@ trackedFiles:
internal/cli/operationwithleadingandtrailingunderscores.go:
last_write_checksum: sha1:cf1e07bbdf0dabdb9fffd15c9e3f47a018a59f80
internal/cli/parenthesesinpathallowed.go:
- last_write_checksum: sha1:adee137db3ff5c047823800c99412ecab849f706
+ last_write_checksum: sha1:044a262b30d750b1a82bc33ab31a70195cab61dd
internal/cli/postfile.go:
last_write_checksum: sha1:f67d5ddcaa3ea921513ee6f97fe5eb8db9580aae
internal/cli/renderasset.go:
- last_write_checksum: sha1:66703b4e61cf22941907d9a373dd4e75587f7e0e
+ last_write_checksum: sha1:7b64189912eaa0e2c11bdbc91f39d3d035bbc8b3
internal/cli/root.go:
last_write_checksum: sha1:4efdf5323f50a1dc87bd20d3ffe86af0804da2fa
internal/cli/tag1/auth.go:
@@ -196,21 +196,21 @@ trackedFiles:
internal/cli/tag1/root.go:
last_write_checksum: sha1:cfe60d600cb9609d9609056bfce8fb7c97134b41
internal/cli/testendpoint.go:
- last_write_checksum: sha1:264efb2b7a2045e94fbec8f4976db14e1018b9d8
+ last_write_checksum: sha1:d4ea63754634f3cb79d9603f3abfd7730968ceb8
internal/cli/testenumformats.go:
- last_write_checksum: sha1:fc0f5c836a4b4ad9d47fd1a903e963a847aec9b2
+ last_write_checksum: sha1:3ed815fb0d60772beecf29e251238985400badd4
internal/cli/testgroup/root.go:
last_write_checksum: sha1:e252ec79b7dccbf3cedb78d27ed2f3f7a4cd819f
internal/cli/testgroup/tag2/posttest.go:
- last_write_checksum: sha1:3cb9926f6fcfa49cdcfff47066a1a0346ec82468
+ last_write_checksum: sha1:1cf593c40b1e581cc5063acd177d2ecbd6ba30ae
internal/cli/testgroup/tag2/root.go:
last_write_checksum: sha1:333e0d1e2947411048be5e44359d41a779a8051d
internal/cli/testgroup/tag3/posttest.go:
- last_write_checksum: sha1:85afaaa2325b939b4299596689986d3cb98649f5
+ last_write_checksum: sha1:67fc1540b046c7acb34da3d95898fdff4590da67
internal/cli/testgroup/tag3/root.go:
last_write_checksum: sha1:544b5d37f18bcd87ed2b878deca97b3dba8da1e7
internal/cli/updateuser.go:
- last_write_checksum: sha1:1b05be0bffcc0bc864e880d820bdf5f1ada3c29e
+ last_write_checksum: sha1:703a7dfa5c3601afeff9e803735a4110c118ef71
internal/cli/urlvalidationstresstest.go:
last_write_checksum: sha1:088e04431ff4780ee3220d86bf8f423def61f7f6
internal/cli/validate.go:
@@ -246,7 +246,7 @@ trackedFiles:
internal/flagutil/flags.go:
last_write_checksum: sha1:7a255e684a2ea0367f68a8683b9167c56a0f4235
internal/flagutil/metadata.go:
- last_write_checksum: sha1:f2dae1449af37ccf1929900338cffde1f07b1220
+ last_write_checksum: sha1:1936c82d8e36a6612364827ffc05dcbea0e1dd8f
internal/flagutil/preset.go:
last_write_checksum: sha1:a580b7d4b00cf469415bc53d40f4981d60091423
internal/interactive/interactive.go:
diff --git a/zSDKs/sdk-cli/internal/cli/binaryandstringupload.go b/zSDKs/sdk-cli/internal/cli/binaryandstringupload.go
index 0d6051ca..b854b8e5 100644
--- a/zSDKs/sdk-cli/internal/cli/binaryandstringupload.go
+++ b/zSDKs/sdk-cli/internal/cli/binaryandstringupload.go
@@ -37,7 +37,7 @@ func initBinaryAndStringUploadCmd(parent *cobra.Command) error {
return fmt.Errorf("invalid metadata for binary-and-string-upload: %w", err)
}
cmd.Flags().String("body", "", "Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.")
- _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true})
+ _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Required: false, Kind: "json", BodyFlag: true})
cmd.Annotations[flagutil.AnnotationWholeBodyFlag] = "body"
if err := flagutil.AnnotateBodyFields(cmd, binaryAndStringUploadCmdMeta, "", "body"); err != nil {
return fmt.Errorf("annotate body fields for binary-and-string-upload: %w", err)
diff --git a/zSDKs/sdk-cli/internal/cli/createuser.go b/zSDKs/sdk-cli/internal/cli/createuser.go
index f582a73b..7efb60a4 100644
--- a/zSDKs/sdk-cli/internal/cli/createuser.go
+++ b/zSDKs/sdk-cli/internal/cli/createuser.go
@@ -46,7 +46,7 @@ func initCreateUserCmd(parent *cobra.Command) error {
return fmt.Errorf("invalid metadata for create-user: %w", err)
}
cmd.Flags().String("body", "", "Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.")
- _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true})
+ _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Required: false, Kind: "json", BodyFlag: true})
cmd.Annotations[flagutil.AnnotationWholeBodyFlag] = "body"
if err := flagutil.AnnotateBodyFields(cmd, createUserCmdMeta, "", "body"); err != nil {
return fmt.Errorf("annotate body fields for create-user: %w", err)
diff --git a/zSDKs/sdk-cli/internal/cli/createwithunion.go b/zSDKs/sdk-cli/internal/cli/createwithunion.go
index f9cd395b..22acaa0b 100644
--- a/zSDKs/sdk-cli/internal/cli/createwithunion.go
+++ b/zSDKs/sdk-cli/internal/cli/createwithunion.go
@@ -49,7 +49,7 @@ func initCreateWithUnionCmd(parent *cobra.Command) error {
return fmt.Errorf("invalid metadata for create-with-union: %w", err)
}
cmd.Flags().String("body", "", "Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.")
- _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true})
+ _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Required: false, Kind: "json", BodyFlag: true})
cmd.Annotations[flagutil.AnnotationWholeBodyFlag] = "body"
if err := flagutil.AnnotateBodyFields(cmd, createWithUnionCmdMeta, "ShapeRequest", "body"); err != nil {
return fmt.Errorf("annotate body fields for create-with-union: %w", err)
diff --git a/zSDKs/sdk-cli/internal/cli/getfullyflattenedrequest.go b/zSDKs/sdk-cli/internal/cli/getfullyflattenedrequest.go
index 470d8ba1..e2337cc2 100644
--- a/zSDKs/sdk-cli/internal/cli/getfullyflattenedrequest.go
+++ b/zSDKs/sdk-cli/internal/cli/getfullyflattenedrequest.go
@@ -42,7 +42,7 @@ func initGetFullyFlattenedRequestCmd(parent *cobra.Command) error {
return fmt.Errorf("invalid metadata for get-fully-flattened-request: %w", err)
}
cmd.Flags().String("body", "", "Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.")
- _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true})
+ _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Required: false, Kind: "json", BodyFlag: true})
cmd.Annotations[flagutil.AnnotationWholeBodyFlag] = "body"
if err := flagutil.AnnotateBodyFields(cmd, getFullyFlattenedRequestCmdMeta, "RequestBody", "body"); err != nil {
return fmt.Errorf("annotate body fields for get-fully-flattened-request: %w", err)
diff --git a/zSDKs/sdk-cli/internal/cli/namespacetests/conflicts/createnamespaceconflict.go b/zSDKs/sdk-cli/internal/cli/namespacetests/conflicts/createnamespaceconflict.go
index fab351a7..876b2379 100644
--- a/zSDKs/sdk-cli/internal/cli/namespacetests/conflicts/createnamespaceconflict.go
+++ b/zSDKs/sdk-cli/internal/cli/namespacetests/conflicts/createnamespaceconflict.go
@@ -44,7 +44,7 @@ func initCreateNamespaceConflictCmd(parent *cobra.Command) error {
return fmt.Errorf("invalid metadata for create-namespace-conflict: %w", err)
}
cmd.Flags().String("body", "", "Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.")
- _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true})
+ _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Required: false, Kind: "json", BodyFlag: true})
cmd.Annotations[flagutil.AnnotationWholeBodyFlag] = "body"
if err := flagutil.AnnotateBodyFields(cmd, createNamespaceConflictCmdMeta, "", "body"); err != nil {
return fmt.Errorf("annotate body fields for create-namespace-conflict: %w", err)
diff --git a/zSDKs/sdk-cli/internal/cli/namespacetests/conflicts/putnamespaceconflict.go b/zSDKs/sdk-cli/internal/cli/namespacetests/conflicts/putnamespaceconflict.go
index 8ef15aab..df7365b0 100644
--- a/zSDKs/sdk-cli/internal/cli/namespacetests/conflicts/putnamespaceconflict.go
+++ b/zSDKs/sdk-cli/internal/cli/namespacetests/conflicts/putnamespaceconflict.go
@@ -40,7 +40,7 @@ func initPutNamespaceConflictCmd(parent *cobra.Command) error {
return fmt.Errorf("invalid metadata for put-namespace-conflict: %w", err)
}
cmd.Flags().String("body", "", "Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.")
- _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true})
+ _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Required: false, Kind: "json", BodyFlag: true})
cmd.Annotations[flagutil.AnnotationWholeBodyFlag] = "body"
if err := flagutil.AnnotateBodyFields(cmd, putNamespaceConflictCmdMeta, "", "body"); err != nil {
return fmt.Errorf("annotate body fields for put-namespace-conflict: %w", err)
diff --git a/zSDKs/sdk-cli/internal/cli/namespacetests/singlefoo/createsinglenamespacefoopet.go b/zSDKs/sdk-cli/internal/cli/namespacetests/singlefoo/createsinglenamespacefoopet.go
index f87e52fb..8d977f30 100644
--- a/zSDKs/sdk-cli/internal/cli/namespacetests/singlefoo/createsinglenamespacefoopet.go
+++ b/zSDKs/sdk-cli/internal/cli/namespacetests/singlefoo/createsinglenamespacefoopet.go
@@ -44,7 +44,7 @@ func initCreateSingleNamespaceFooPetCmd(parent *cobra.Command) error {
return fmt.Errorf("invalid metadata for create-single-namespace-foo-pet: %w", err)
}
cmd.Flags().String("body", "", "Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.")
- _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true})
+ _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Required: false, Kind: "json", BodyFlag: true})
cmd.Annotations[flagutil.AnnotationWholeBodyFlag] = "body"
if err := flagutil.AnnotateBodyFields(cmd, createSingleNamespaceFooPetCmdMeta, "", "body"); err != nil {
return fmt.Errorf("annotate body fields for create-single-namespace-foo-pet: %w", err)
diff --git a/zSDKs/sdk-cli/internal/cli/parenthesesinpathallowed.go b/zSDKs/sdk-cli/internal/cli/parenthesesinpathallowed.go
index c7661fe6..a35e2cef 100644
--- a/zSDKs/sdk-cli/internal/cli/parenthesesinpathallowed.go
+++ b/zSDKs/sdk-cli/internal/cli/parenthesesinpathallowed.go
@@ -41,7 +41,7 @@ func initParenthesesInPathAllowedCmd(parent *cobra.Command) error {
return fmt.Errorf("invalid metadata for parentheses-in-path-allowed: %w", err)
}
cmd.Flags().String("body", "", "Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.")
- _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true})
+ _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Required: false, Kind: "json", BodyFlag: true})
cmd.Annotations[flagutil.AnnotationWholeBodyFlag] = "body"
if err := flagutil.AnnotateBodyFields(cmd, parenthesesInPathAllowedCmdMeta, "TemplateBracesTest", "body"); err != nil {
return fmt.Errorf("annotate body fields for parentheses-in-path-allowed: %w", err)
diff --git a/zSDKs/sdk-cli/internal/cli/renderasset.go b/zSDKs/sdk-cli/internal/cli/renderasset.go
index 0861dc8c..ca496bb2 100644
--- a/zSDKs/sdk-cli/internal/cli/renderasset.go
+++ b/zSDKs/sdk-cli/internal/cli/renderasset.go
@@ -37,7 +37,7 @@ func initRenderAssetCmd(parent *cobra.Command) error {
return fmt.Errorf("invalid metadata for render-asset: %w", err)
}
cmd.Flags().String("body", "", "Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.")
- _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true})
+ _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Required: false, Kind: "json", BodyFlag: true})
cmd.Annotations[flagutil.AnnotationWholeBodyFlag] = "body"
if err := flagutil.AnnotateBodyFields(cmd, renderAssetCmdMeta, "", "body"); err != nil {
return fmt.Errorf("annotate body fields for render-asset: %w", err)
diff --git a/zSDKs/sdk-cli/internal/cli/testendpoint.go b/zSDKs/sdk-cli/internal/cli/testendpoint.go
index 603db0e6..1c3acddf 100644
--- a/zSDKs/sdk-cli/internal/cli/testendpoint.go
+++ b/zSDKs/sdk-cli/internal/cli/testendpoint.go
@@ -38,7 +38,7 @@ func initTestEndpointCmd(parent *cobra.Command) error {
return fmt.Errorf("invalid metadata for test-endpoint: %w", err)
}
cmd.Flags().String("body", "", "Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.")
- _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true})
+ _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Required: false, Kind: "json", BodyFlag: true})
cmd.Annotations[flagutil.AnnotationWholeBodyFlag] = "body"
if err := flagutil.AnnotateBodyFields(cmd, testEndpointCmdMeta, "RequestBody", "body"); err != nil {
return fmt.Errorf("annotate body fields for test-endpoint: %w", err)
diff --git a/zSDKs/sdk-cli/internal/cli/testenumformats.go b/zSDKs/sdk-cli/internal/cli/testenumformats.go
index c2829c29..6e980ebb 100644
--- a/zSDKs/sdk-cli/internal/cli/testenumformats.go
+++ b/zSDKs/sdk-cli/internal/cli/testenumformats.go
@@ -40,7 +40,7 @@ func initTestEnumFormatsCmd(parent *cobra.Command) error {
return fmt.Errorf("invalid metadata for test-enum-formats: %w", err)
}
cmd.Flags().String("body", "", "Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.")
- _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true})
+ _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Required: false, Kind: "json", BodyFlag: true})
cmd.Annotations[flagutil.AnnotationWholeBodyFlag] = "body"
if err := flagutil.AnnotateBodyFields(cmd, testEnumFormatsCmdMeta, "", "body"); err != nil {
return fmt.Errorf("annotate body fields for test-enum-formats: %w", err)
diff --git a/zSDKs/sdk-cli/internal/cli/testgroup/tag2/posttest.go b/zSDKs/sdk-cli/internal/cli/testgroup/tag2/posttest.go
index b9a2b980..bf480252 100644
--- a/zSDKs/sdk-cli/internal/cli/testgroup/tag2/posttest.go
+++ b/zSDKs/sdk-cli/internal/cli/testgroup/tag2/posttest.go
@@ -36,7 +36,7 @@ func initPostTestCmd(parent *cobra.Command) error {
return fmt.Errorf("invalid metadata for post-test: %w", err)
}
cmd.Flags().String("body", "", "Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.")
- _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true})
+ _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Required: false, Kind: "json", BodyFlag: true})
cmd.Annotations[flagutil.AnnotationWholeBodyFlag] = "body"
if err := flagutil.AnnotateBodyFields(cmd, postTestCmdMeta, "Test2Request", "body"); err != nil {
return fmt.Errorf("annotate body fields for post-test: %w", err)
diff --git a/zSDKs/sdk-cli/internal/cli/testgroup/tag3/posttest.go b/zSDKs/sdk-cli/internal/cli/testgroup/tag3/posttest.go
index 57e6f7fe..7e44ef34 100644
--- a/zSDKs/sdk-cli/internal/cli/testgroup/tag3/posttest.go
+++ b/zSDKs/sdk-cli/internal/cli/testgroup/tag3/posttest.go
@@ -36,7 +36,7 @@ func initPostTestCmd(parent *cobra.Command) error {
return fmt.Errorf("invalid metadata for post-test: %w", err)
}
cmd.Flags().String("body", "", "Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.")
- _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true})
+ _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Required: false, Kind: "json", BodyFlag: true})
cmd.Annotations[flagutil.AnnotationWholeBodyFlag] = "body"
if err := flagutil.AnnotateBodyFields(cmd, postTestCmdMeta, "Test2Request", "body"); err != nil {
return fmt.Errorf("annotate body fields for post-test: %w", err)
diff --git a/zSDKs/sdk-cli/internal/cli/updateuser.go b/zSDKs/sdk-cli/internal/cli/updateuser.go
index 34407f7c..43338982 100644
--- a/zSDKs/sdk-cli/internal/cli/updateuser.go
+++ b/zSDKs/sdk-cli/internal/cli/updateuser.go
@@ -47,7 +47,7 @@ func initUpdateUserCmd(parent *cobra.Command) error {
return fmt.Errorf("invalid metadata for update-user: %w", err)
}
cmd.Flags().String("body", "", "Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.")
- _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true})
+ _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Required: false, Kind: "json", BodyFlag: true})
cmd.Annotations[flagutil.AnnotationWholeBodyFlag] = "body"
if err := flagutil.AnnotateBodyFields(cmd, updateUserCmdMeta, "User", "body"); err != nil {
return fmt.Errorf("annotate body fields for update-user: %w", err)
diff --git a/zSDKs/sdk-cli/internal/flagutil/metadata.go b/zSDKs/sdk-cli/internal/flagutil/metadata.go
index e91fc06c..b93279e2 100644
--- a/zSDKs/sdk-cli/internal/flagutil/metadata.go
+++ b/zSDKs/sdk-cli/internal/flagutil/metadata.go
@@ -664,8 +664,16 @@ func BuildRequest[T any](cmd *cobra.Command, meta []FlagMeta, bodyFieldPath stri
v := reflect.ValueOf(&req).Elem()
bodyPrePopulated := false
hasRequestBody := bodyFieldPath != "" || bodyFlagName != "" || !isJSONSerialized(v.Type())
+ bodyRequired := false
+ if flag := cmd.Flags().Lookup(bodyFlagName); flag != nil {
+ values := flag.Annotations[AnnotationRequired]
+ bodyRequired = len(values) > 0 && values[0] == "true"
+ }
decodeBody := func(data []byte, source string) error {
+ if bodyRequired && bytes.Equal(bytes.TrimSpace(data), []byte("null")) {
+ return WithCLIValidation(fmt.Errorf("invalid value for --%s: null; the body is required", bodyFlagName))
+ }
u := bodyUnionMeta(meta, bodyFieldPath)
if bodyFieldPath != "" {
bodyField, err := navigateToField(v, bodyFieldPath)
@@ -725,6 +733,10 @@ func BuildRequest[T any](cmd *cobra.Command, meta []FlagMeta, bodyFieldPath stri
}
}
+ if bodyRequired && !bodyPrePopulated {
+ return nil, &MissingRequiredFlagError{FlagName: bodyFlagName, Detail: "(or provide via stdin)"}
+ }
+
// When body provided via --body flag or stdin, relax Required checks for body fields
// so builders don't error for fields already populated
if bodyPrePopulated {