Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to custom marshal map keys in JSON

I can't understand a strange behavior of custom marshal int to string.

Here is an example:

package main

import (
    "encoding/json"
    "fmt"
)

type Int int

func (a Int) MarshalJSON() ([]byte, error) {
    test := a / 10
    return json.Marshal(fmt.Sprintf("%d-%d", a, test))
}

func main() {

    array := []Int{100, 200}
    arrayJson, _ := json.Marshal(array)
    fmt.Println("array", string(arrayJson))

    maps := map[Int]bool{
        100: true,
        200: true,
    }
    mapsJson, _ := json.Marshal(maps)
    fmt.Println("map wtf?", string(mapsJson))
    fmt.Println("map must be:", `{"100-10":true, "200-20":true}`)
}

The output is:

array ["100-10","200-20"]
map wtf? {"100":true,"200":true}
map must be: {"100-10":true, "200-20":true}

https://play.golang.org/p/iiUyL2Hc5h_P

What am I missing?

like image 376
Martin Prestone Avatar asked Sep 04 '18 08:09

Martin Prestone


1 Answers

This is the expected outcome, which is documented at json.Marshal():

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 sorted and 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

Note that map keys are handled differently than values of properties because map keys in JSON are the property names which are always string values (while property values may be JSON text, number and boolean values).

As per the doc, if you want it to work for map keys as well, implement encoding.TextMarshaler:

func (a Int) MarshalText() (text []byte, err error) {
    test := a / 10
    return []byte(fmt.Sprintf("%d-%d", a, test)), nil
}

(Note that MarshalText() is ought to return "just" simple text, not JSON text, hence we omit JSON marshaling in it!)

With this, output will be (try it on the Go Playground):

array ["100-10","200-20"] <nil>
map wtf? {"100-10":true,"200-20":true} <nil>
map must be: {"100-10":true, "200-20":true}

Note that encoding.TextMarshaler is enough as that is also checked when marsaling as values, not just for map keys. So you don't have to implement both encoding.TextMarshaler and json.Marshaler.

If you do implement both, you can have different output when the value is marshaled as a "simple" value and as a map key because json.Marshaler takes precedence when generating a value:

func (a Int) MarshalJSON() ([]byte, error) {
    test := a / 100
    return json.Marshal(fmt.Sprintf("%d-%d", a, test))
}

func (a Int) MarshalText() (text []byte, err error) {
    test := a / 10
    return []byte(fmt.Sprintf("%d-%d", a, test)), nil
}

This time the output will be (try it on the Go Playground):

array ["100-1","200-2"] <nil>
map wtf? {"100-10":true,"200-20":true} <nil>
map must be: {"100-10":true, "200-20":true}
like image 128
icza Avatar answered Nov 12 '22 12:11

icza