@@ -15,6 +15,9 @@ import (
1515 "math/big"
1616 "reflect"
1717 "slices"
18+ "sync"
19+
20+ "github.com/modelcontextprotocol/go-sdk/internal/util"
1821)
1922
2023// Equal reports whether two Go values representing JSON values are equal according
@@ -282,3 +285,136 @@ func assert(cond bool, msg string) {
282285 panic ("assertion failed: " + msg )
283286 }
284287}
288+
289+ // marshalStructWithMap marshals its first argument to JSON, treating the field named
290+ // mapField as an embedded map. The first argument must be a pointer to
291+ // a struct. The underlying type of mapField must be a map[string]any, and it must have
292+ // a "-" json tag, meaning it will not be marshaled.
293+ //
294+ // For example, given this struct:
295+ //
296+ // type S struct {
297+ // A int
298+ // Extra map[string] any `json:"-"`
299+ // }
300+ //
301+ // and this value:
302+ //
303+ // s := S{A: 1, Extra: map[string]any{"B": 2}}
304+ //
305+ // the call marshalJSONWithMap(s, "Extra") would return
306+ //
307+ // {"A": 1, "B": 2}
308+ //
309+ // It is an error if the map contains the same key as another struct field's
310+ // JSON name.
311+ //
312+ // marshalStructWithMap calls json.Marshal on a value of type T, so T must not
313+ // have a MarshalJSON method that calls this function, on pain of infinite regress.
314+ //
315+ // Note that there is a similar function in mcp/util.go, but they are not the same.
316+ // Here the function requires `-` json tag, does not clear the mapField map,
317+ // and handles embedded struct due to the implementation of jsonNames in this package.
318+ //
319+ // TODO: avoid this restriction on T by forcing it to marshal in a default way.
320+ // See https://go.dev/play/p/EgXKJHxEx_R.
321+ func marshalStructWithMap [T any ](s * T , mapField string ) ([]byte , error ) {
322+ // Marshal the struct and the map separately, and concatenate the bytes.
323+ // This strategy is dramatically less complicated than
324+ // constructing a synthetic struct or map with the combined keys.
325+ if s == nil {
326+ return []byte ("null" ), nil
327+ }
328+ s2 := * s
329+ vMapField := reflect .ValueOf (& s2 ).Elem ().FieldByName (mapField )
330+ mapVal := vMapField .Interface ().(map [string ]any )
331+
332+ // Check for duplicates.
333+ names := jsonNames (reflect .TypeFor [T ]())
334+ for key := range mapVal {
335+ if names [key ] {
336+ return nil , fmt .Errorf ("map key %q duplicates struct field" , key )
337+ }
338+ }
339+
340+ structBytes , err := json .Marshal (s2 )
341+ if err != nil {
342+ return nil , fmt .Errorf ("marshalStructWithMap(%+v): %w" , s , err )
343+ }
344+ if len (mapVal ) == 0 {
345+ return structBytes , nil
346+ }
347+ mapBytes , err := json .Marshal (mapVal )
348+ if err != nil {
349+ return nil , err
350+ }
351+ if len (structBytes ) == 2 { // must be "{}"
352+ return mapBytes , nil
353+ }
354+ // "{X}" + "{Y}" => "{X,Y}"
355+ res := append (structBytes [:len (structBytes )- 1 ], ',' )
356+ res = append (res , mapBytes [1 :]... )
357+ return res , nil
358+ }
359+
360+ // unmarshalStructWithMap is the inverse of marshalStructWithMap.
361+ // T has the same restrictions as in that function.
362+ //
363+ // Note that there is a similar function in mcp/util.go, but they are not the same.
364+ // Here jsonNames also returns fields from embedded structs, hence this function
365+ // handles embedded structs as well.
366+ func unmarshalStructWithMap [T any ](data []byte , v * T , mapField string ) error {
367+ // Unmarshal into the struct, ignoring unknown fields.
368+ if err := json .Unmarshal (data , v ); err != nil {
369+ return err
370+ }
371+ // Unmarshal into the map.
372+ m := map [string ]any {}
373+ if err := json .Unmarshal (data , & m ); err != nil {
374+ return err
375+ }
376+ // Delete from the map the fields of the struct.
377+ for n := range jsonNames (reflect .TypeFor [T ]()) {
378+ delete (m , n )
379+ }
380+ if len (m ) != 0 {
381+ reflect .ValueOf (v ).Elem ().FieldByName (mapField ).Set (reflect .ValueOf (m ))
382+ }
383+ return nil
384+ }
385+
386+ var jsonNamesMap sync.Map // from reflect.Type to map[string]bool
387+
388+ // jsonNames returns the set of JSON object keys that t will marshal into,
389+ // including fields from embedded structs in t.
390+ // t must be a struct type.
391+ //
392+ // Note that there is a similar function in mcp/util.go, but they are not the same
393+ // Here the function recurses over embedded structs and includes fields from them.
394+ func jsonNames (t reflect.Type ) map [string ]bool {
395+ // Lock not necessary: at worst we'll duplicate work.
396+ if val , ok := jsonNamesMap .Load (t ); ok {
397+ return val .(map [string ]bool )
398+ }
399+ m := map [string ]bool {}
400+ for i := range t .NumField () {
401+ field := t .Field (i )
402+ // handle embedded structs
403+ if field .Anonymous {
404+ fieldType := field .Type
405+ if fieldType .Kind () == reflect .Ptr {
406+ fieldType = fieldType .Elem ()
407+ }
408+ for n := range jsonNames (fieldType ) {
409+ m [n ] = true
410+ }
411+ continue
412+ }
413+ info := util .FieldJSONInfo (field )
414+ if ! info .Omit {
415+ m [info .Name ] = true
416+ }
417+ }
418+ jsonNamesMap .Store (t , m )
419+ return m
420+ }
0 commit comments