]> Cypherpunks repositories - gostls13.git/commitdiff
encoding/json: support maps with integer keys
authorCaleb Spare <cespare@gmail.com>
Wed, 13 Apr 2016 23:51:25 +0000 (16:51 -0700)
committerBrad Fitzpatrick <bradfitz@golang.org>
Tue, 10 May 2016 03:53:12 +0000 (03:53 +0000)
This change makes encoding and decoding support integer types in map
keys, converting to/from JSON string keys.

JSON object keys are still sorted lexically, even though the keys may be
integer strings.

For backwards-compatibility, the existing Text(Un)Marshaler support for
map keys (added in CL 20356) does not take precedence over the default
encoding for string types. There is no such concern for integer types,
so integer map key encoding is only used as a fallback if the map key
type is not a Text(Un)Marshaler.

Fixes #12529.

Change-Id: I7e68c34f9cd19704b1d233a9862da15fabf0908a
Reviewed-on: https://go-review.googlesource.com/22060
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
src/encoding/json/decode.go
src/encoding/json/decode_test.go
src/encoding/json/encode.go

index 434edf8ea450967cb4fdf9edeadf6048860b4db1..2eda875bfdee8ab10080f096357891a8c49f2dfb 100644 (file)
@@ -62,10 +62,10 @@ import (
 // the additional Go array elements are set to zero values.
 //
 // To unmarshal a JSON object into a map, Unmarshal first establishes a map to
-// use, If the map is nil, Unmarshal allocates a new map. Otherwise Unmarshal
+// use. If the map is nil, Unmarshal allocates a new map. Otherwise Unmarshal
 // reuses the existing map, keeping existing entries. Unmarshal then stores key-
-// value pairs from the JSON object into the map.  The map's key type must
-// either be a string or implement encoding.TextUnmarshaler.
+// value pairs from the JSON object into the map. The map's key type must
+// either be a string, an integer, or implement encoding.TextUnmarshaler.
 //
 // If a JSON value is not appropriate for a given target type,
 // or if a JSON number overflows the target type, Unmarshal
@@ -581,17 +581,24 @@ func (d *decodeState) object(v reflect.Value) {
 
        // Check type of target:
        //   struct or
-       //   map[string]T or map[encoding.TextUnmarshaler]T
+       //   map[T1]T2 where T1 is string, an integer type,
+       //             or an encoding.TextUnmarshaler
        switch v.Kind() {
        case reflect.Map:
-               // Map key must either have string kind or be an encoding.TextUnmarshaler.
+               // Map key must either have string kind, have an integer kind,
+               // or be an encoding.TextUnmarshaler.
                t := v.Type()
-               if t.Key().Kind() != reflect.String &&
-                       !reflect.PtrTo(t.Key()).Implements(textUnmarshalerType) {
-                       d.saveError(&UnmarshalTypeError{"object", v.Type(), int64(d.off)})
-                       d.off--
-                       d.next() // skip over { } in input
-                       return
+               switch t.Key().Kind() {
+               case reflect.String,
+                       reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
+                       reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+               default:
+                       if !reflect.PtrTo(t.Key()).Implements(textUnmarshalerType) {
+                               d.saveError(&UnmarshalTypeError{"object", v.Type(), int64(d.off)})
+                               d.off--
+                               d.next() // skip over { } in input
+                               return
+                       }
                }
                if v.IsNil() {
                        v.Set(reflect.MakeMap(t))
@@ -696,13 +703,32 @@ func (d *decodeState) object(v reflect.Value) {
                        var kv reflect.Value
                        switch {
                        case kt.Kind() == reflect.String:
-                               kv = reflect.ValueOf(key).Convert(v.Type().Key())
+                               kv = reflect.ValueOf(key).Convert(kt)
                        case reflect.PtrTo(kt).Implements(textUnmarshalerType):
                                kv = reflect.New(v.Type().Key())
                                d.literalStore(item, kv, true)
                                kv = kv.Elem()
                        default:
-                               panic("json: Unexpected key type") // should never occur
+                               switch kt.Kind() {
+                               case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+                                       s := string(key)
+                                       n, err := strconv.ParseInt(s, 10, 64)
+                                       if err != nil || reflect.Zero(kt).OverflowInt(n) {
+                                               d.saveError(&UnmarshalTypeError{"number " + s, kt, int64(start + 1)})
+                                               return
+                                       }
+                                       kv = reflect.ValueOf(n).Convert(kt)
+                               case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+                                       s := string(key)
+                                       n, err := strconv.ParseUint(s, 10, 64)
+                                       if err != nil || reflect.Zero(kt).OverflowUint(n) {
+                                               d.saveError(&UnmarshalTypeError{"number " + s, kt, int64(start + 1)})
+                                               return
+                                       }
+                                       kv = reflect.ValueOf(n).Convert(kt)
+                               default:
+                                       panic("json: Unexpected key type") // should never occur
+                               }
                        }
                        v.SetMapIndex(kv, subv)
                }
index 30e46ca44f07bdd5d41fa8afda11758e5807024f..7c388c0c272ff6654a08bc1e4fe221ff5172ca77 100644 (file)
@@ -10,8 +10,10 @@ import (
        "errors"
        "fmt"
        "image"
+       "math"
        "net"
        "reflect"
+       "strconv"
        "strings"
        "testing"
        "time"
@@ -53,6 +55,8 @@ type tx struct {
        x int
 }
 
+type u8 uint8
+
 // A type that can unmarshal itself.
 
 type unmarshaler struct {
@@ -92,6 +96,29 @@ type ustructText struct {
        M unmarshalerText
 }
 
+// u8marshal is an integer type that can marshal/unmarshal itself.
+type u8marshal uint8
+
+func (u8 u8marshal) MarshalText() ([]byte, error) {
+       return []byte(fmt.Sprintf("u%d", u8)), nil
+}
+
+var errMissingU8Prefix = errors.New("missing 'u' prefix")
+
+func (u8 *u8marshal) UnmarshalText(b []byte) error {
+       if !bytes.HasPrefix(b, []byte{'u'}) {
+               return errMissingU8Prefix
+       }
+       n, err := strconv.Atoi(string(b[1:]))
+       if err != nil {
+               return err
+       }
+       *u8 = u8marshal(n)
+       return nil
+}
+
+var _ encoding.TextUnmarshaler = (*u8marshal)(nil)
+
 var (
        um0, um1 unmarshaler // target2 of unmarshaling
        ump      = &um1
@@ -320,7 +347,69 @@ var unmarshalTests = []unmarshalTest{
        {in: `["x:y"]`, ptr: &umslicepType, out: &umsliceXY},
        {in: `{"M":"x:y"}`, ptr: umstructType, out: umstructXY},
 
-       // Map keys can be encoding.TextUnmarshalers
+       // integer-keyed map test
+       {
+               in:  `{"-1":"a","0":"b","1":"c"}`,
+               ptr: new(map[int]string),
+               out: map[int]string{-1: "a", 0: "b", 1: "c"},
+       },
+       {
+               in:  `{"0":"a","10":"c","9":"b"}`,
+               ptr: new(map[u8]string),
+               out: map[u8]string{0: "a", 9: "b", 10: "c"},
+       },
+       {
+               in:  `{"-9223372036854775808":"min","9223372036854775807":"max"}`,
+               ptr: new(map[int64]string),
+               out: map[int64]string{math.MinInt64: "min", math.MaxInt64: "max"},
+       },
+       {
+               in:  `{"18446744073709551615":"max"}`,
+               ptr: new(map[uint64]string),
+               out: map[uint64]string{math.MaxUint64: "max"},
+       },
+       {
+               in:  `{"0":false,"10":true}`,
+               ptr: new(map[uintptr]bool),
+               out: map[uintptr]bool{0: false, 10: true},
+       },
+
+       // Check that MarshalText and UnmarshalText take precedence
+       // over default integer handling in map keys.
+       {
+               in:  `{"u2":4}`,
+               ptr: new(map[u8marshal]int),
+               out: map[u8marshal]int{2: 4},
+       },
+       {
+               in:  `{"2":4}`,
+               ptr: new(map[u8marshal]int),
+               err: errMissingU8Prefix,
+       },
+
+       // integer-keyed map errors
+       {
+               in:  `{"abc":"abc"}`,
+               ptr: new(map[int]string),
+               err: &UnmarshalTypeError{"number abc", reflect.TypeOf(0), 2},
+       },
+       {
+               in:  `{"256":"abc"}`,
+               ptr: new(map[uint8]string),
+               err: &UnmarshalTypeError{"number 256", reflect.TypeOf(uint8(0)), 2},
+       },
+       {
+               in:  `{"128":"abc"}`,
+               ptr: new(map[int8]string),
+               err: &UnmarshalTypeError{"number 128", reflect.TypeOf(int8(0)), 2},
+       },
+       {
+               in:  `{"-1":"abc"}`,
+               ptr: new(map[uint8]string),
+               err: &UnmarshalTypeError{"number -1", reflect.TypeOf(uint8(0)), 2},
+       },
+
+       // Map keys can be encoding.TextUnmarshalers.
        {in: `{"x:y":true}`, ptr: &ummapType, out: ummapXY},
        // If multiple values for the same key exists, only the most recent value is used.
        {in: `{"x:y":false,"x:y":true}`, ptr: &ummapType, out: ummapXY},
index d8c779869b33f89be3d6e5964ce06763a7aba930..8b967471ce70fcf120d85c1dc738b48e41e67313 100644 (file)
@@ -117,9 +117,13 @@ import (
 // an anonymous struct field in both current and earlier versions, give the field
 // a JSON tag of "-".
 //
-// Map values encode as JSON objects. The map's key type must either be a string
-// or implement encoding.TextMarshaler.  The map keys are used as JSON object
-// keys, subject to the UTF-8 coercion described for string values above.
+// Map values encode as JSON objects. The map's key type must either be a
+// string, an integer type, or implement encoding.TextMarshaler. The map keys
+// are used as JSON object keys by applying the following rules, subject to the
+// UTF-8 coercion described for string values above:
+//   - string keys are used directly
+//   - encoding.TextMarshalers are marshaled
+//   - integer keys are converted to strings
 //
 // Pointer values encode as the value pointed to.
 // A nil pointer encodes as the null JSON value.
@@ -644,8 +648,14 @@ func (me *mapEncoder) encode(e *encodeState, v reflect.Value, opts encOpts) {
 }
 
 func newMapEncoder(t reflect.Type) encoderFunc {
-       if t.Key().Kind() != reflect.String && !t.Key().Implements(textMarshalerType) {
-               return unsupportedTypeEncoder
+       switch t.Key().Kind() {
+       case reflect.String,
+               reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
+               reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+       default:
+               if !t.Key().Implements(textMarshalerType) {
+                       return unsupportedTypeEncoder
+               }
        }
        me := &mapEncoder{typeEncoder(t.Elem())}
        return me.encode
@@ -806,9 +816,20 @@ func (w *reflectWithString) resolve() error {
                w.s = w.v.String()
                return nil
        }
-       buf, err := w.v.Interface().(encoding.TextMarshaler).MarshalText()
-       w.s = string(buf)
-       return err
+       if tm, ok := w.v.Interface().(encoding.TextMarshaler); ok {
+               buf, err := tm.MarshalText()
+               w.s = string(buf)
+               return err
+       }
+       switch w.v.Kind() {
+       case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+               w.s = strconv.FormatInt(w.v.Int(), 10)
+               return nil
+       case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+               w.s = strconv.FormatUint(w.v.Uint(), 10)
+               return nil
+       }
+       panic("unexpected map key type")
 }
 
 // byString is a slice of reflectWithString where the reflect.Value is either