package cart import ( "encoding/json" "reflect" "strings" ) // cartItemAlias mirrors CartItem but has no custom (Un)MarshalJSON, so it is // used to (de)serialize the typed fields without recursing. type cartItemAlias CartItem // knownItemKeys is the set of top-level JSON object keys owned by typed // CartItem fields. Any other key in an item object is dynamic and kept in // CartItem.Extra. var knownItemKeys = buildKnownItemKeys() func buildKnownItemKeys() map[string]struct{} { keys := make(map[string]struct{}) t := reflect.TypeOf(CartItem{}) for i := 0; i < t.NumField(); i++ { name, _, _ := strings.Cut(t.Field(i).Tag.Get("json"), ",") if name == "" { name = t.Field(i).Name // untagged fields serialize under their Go name } if name == "-" { continue // e.g. Extra itself } keys[name] = struct{}{} } return keys } // MarshalJSON emits the typed fields and flattens Extra onto the same object. // Typed fields take precedence on key collisions. func (c CartItem) MarshalJSON() ([]byte, error) { core, err := json.Marshal(cartItemAlias(c)) if err != nil || len(c.Extra) == 0 { return core, err } merged := make(map[string]json.RawMessage, len(c.Extra)+8) if err := json.Unmarshal(core, &merged); err != nil { return nil, err } for k, v := range c.Extra { if _, taken := merged[k]; taken { continue } merged[k] = v } return json.Marshal(merged) } // UnmarshalJSON reads the typed fields and collects every other key into Extra. func (c *CartItem) UnmarshalJSON(data []byte) error { var alias cartItemAlias if err := json.Unmarshal(data, &alias); err != nil { return err } *c = CartItem(alias) all := make(map[string]json.RawMessage) if err := json.Unmarshal(data, &all); err != nil { return err } for k := range all { if _, known := knownItemKeys[k]; known { delete(all, k) } } if len(all) > 0 { c.Extra = all } else { c.Extra = nil } return nil }