@@ -5,8 +5,10 @@ import (
55 "encoding/json"
66 "fmt"
77 "io"
8+ "maps"
89 "os"
910 "reflect"
11+ "slices"
1012 "strconv"
1113 "strings"
1214 "time"
@@ -215,7 +217,11 @@ func genFoldPositional(args []string, body map[string]any, wire, kind string) er
215217// command un-driveable. After the json pass, additively bind any url-tagged
216218// field from the body by its url wire-name. For POST structs the url pass is a
217219// no-op, so existing behavior is unchanged.
218- func genBindBody (body map [string ]any , req any ) error {
220+ //
221+ // json.Unmarshal discards body fields the request struct has no field for, so a
222+ // --data field newer than this build's SDK would vanish before the request is
223+ // sent while the call still succeeds. Each such field is named on warn instead.
224+ func genBindBody (warn io.Writer , body map [string ]any , req any ) error {
219225 var missing []string
220226 var inspect func (reflect.Type )
221227 inspect = func (rt reflect.Type ) {
@@ -271,9 +277,103 @@ func genBindBody(body map[string]any, req any) error {
271277 return fmt .Errorf ("failed to bind request: %w" , err )
272278 }
273279 bindURLTagged (body , reflect .ValueOf (req ))
280+ if dropped := unboundFields (body , reflect .TypeOf (req ), "" ); len (dropped ) > 0 {
281+ _ , _ = fmt .Fprintf (warn , "warning: --data fields not sent, unknown to this CLI version: %s\n " ,
282+ strings .Join (dropped , ", " ))
283+ }
274284 return nil
275285}
276286
287+ var jsonUnmarshalerType = reflect .TypeFor [json.Unmarshaler ]()
288+
289+ // unboundFields lists the paths in v (dotted, "[]" for array elements) that
290+ // json.Unmarshal into t discards because t has no field for them. Types that
291+ // decode themselves (json.RawMessage, time.Time) and interface/scalar targets
292+ // take whatever they are given, so nothing below them is reported.
293+ func unboundFields (v any , t reflect.Type , path string ) []string {
294+ for t .Kind () == reflect .Pointer {
295+ t = t .Elem ()
296+ }
297+ if reflect .PointerTo (t ).Implements (jsonUnmarshalerType ) {
298+ return nil
299+ }
300+ var out []string
301+ switch t .Kind () {
302+ case reflect .Struct :
303+ m , _ := v .(map [string ]any )
304+ for _ , k := range slices .Sorted (maps .Keys (m )) {
305+ p := joinFieldPath (path , k )
306+ ft , ok := wireField (t , k )
307+ if ! ok {
308+ out = append (out , p )
309+ continue
310+ }
311+ out = append (out , unboundFields (m [k ], ft , p )... )
312+ }
313+ case reflect .Map :
314+ m , _ := v .(map [string ]any )
315+ for _ , k := range slices .Sorted (maps .Keys (m )) {
316+ out = append (out , unboundFields (m [k ], t .Elem (), joinFieldPath (path , k ))... )
317+ }
318+ case reflect .Slice , reflect .Array :
319+ items , _ := v .([]any )
320+ for _ , item := range items {
321+ out = append (out , unboundFields (item , t .Elem (), path + "[]" )... )
322+ }
323+ slices .Sort (out )
324+ out = slices .Compact (out )
325+ }
326+ return out
327+ }
328+
329+ // wireField resolves a body key to the struct field json.Unmarshal (or, for GET
330+ // query structs, bindURLTagged) would fill: exact wire-name match first, then
331+ // encoding/json's case-insensitive fallback, descending into untagged embedded
332+ // structs.
333+ func wireField (t reflect.Type , key string ) (reflect.Type , bool ) {
334+ var fold reflect.Type
335+ var walk func (reflect.Type ) (reflect.Type , bool )
336+ walk = func (t reflect.Type ) (reflect.Type , bool ) {
337+ for i := 0 ; i < t .NumField (); i ++ {
338+ f := t .Field (i )
339+ tag := f .Tag .Get ("json" )
340+ if tag == "" {
341+ tag = f .Tag .Get ("url" )
342+ }
343+ name := strings .Split (tag , "," )[0 ]
344+ if f .Anonymous && name == "" {
345+ et := f .Type
346+ for et .Kind () == reflect .Pointer {
347+ et = et .Elem ()
348+ }
349+ if et .Kind () == reflect .Struct {
350+ if ft , ok := walk (et ); ok {
351+ return ft , true
352+ }
353+ continue
354+ }
355+ }
356+ if ! f .IsExported () || name == "-" {
357+ continue
358+ }
359+ if name == "" {
360+ name = f .Name
361+ }
362+ if name == key {
363+ return f .Type , true
364+ }
365+ if fold == nil && strings .EqualFold (name , key ) {
366+ fold = f .Type
367+ }
368+ }
369+ return nil , false
370+ }
371+ if ft , ok := walk (t ); ok {
372+ return ft , true
373+ }
374+ return fold , fold != nil
375+ }
376+
277377// bindURLTagged fills fields carrying a `url` struct tag (GET query params) from
278378// the body map keyed by the url wire-name. json.Unmarshal cannot reach these
279379// because they lack a json tag. The 6 GET request types use only int64/string
0 commit comments