如何在JSON中自定义映射键

7

我无法理解自定义 marshal int 到 string 的奇怪行为。

这里有一个例子:

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}`)
}

输出结果为:
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

我错过了什么?

这是一个涉及Go语言的链接。询问者想知道是否有其他需要关注的内容。

2个回答

16
这是预期的结果,文档上已经记录在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
请注意,地图键与属性值处理方式不同,因为JSON中的地图键是属性名称,始终是字符串值(而属性值可以是JSON文本、数字和布尔值)。
根据文档,如果您希望它也适用于地图键,请实现encoding.TextMarshaler
func (a Int) MarshalText() (text []byte, err error) {
    test := a / 10
    return []byte(fmt.Sprintf("%d-%d", a, test)), nil
}

(请注意,MarshalText() 应该返回“仅”简单文本,而不是 JSON 文本,因此我们在其中省略了 JSON 编组!)

有了这个,输出将是(请在 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}

请注意,encoding.TextMarshaler已经足够了,因为在编组值时也会检查它,而不仅仅是用于映射键。因此,您不必同时实现encoding.TextMarshalerjson.Marshaler
如果您确实同时实现了两者,则可以在将值编组为“简单”值和映射键时具有不同的输出,因为json.Marshaler在生成值时具有优先权:
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
}

这次的输出结果将会是(在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}

1

这个被接受的答案非常好,但我已经不止一次地搜索了这个问题,所以我想提供一个包含marshal/unmarshal示例的完整答案,下次我可以直接复制并用作起点 :)

我经常搜索的内容包括:

  • 将自定义类型编码到SQL数据库中
  • 将枚举int作为字符串进行json编码
  • 对map键进行json编码,但不对值进行编码

在此示例中,我创建了一个自定义Weekday类型,该类型匹配time.Weekday int值,但允许请求/响应json和数据库中的字符串值

使用iota可以对任何int枚举执行相同的操作,以在json和数据库中具有可读性强的值

带有测试的Playground示例: https://go.dev/play/p/aUxxIJ6tY9K

重要的部分在于这里:

var (
    // read/write from/to json values
    _ json.Marshaler   = (*Weekday)(nil)
    _ json.Unmarshaler = (*Weekday)(nil)

    // read/write from/to json keys
    _ encoding.TextMarshaler   = (*Weekday)(nil)
    _ encoding.TextUnmarshaler = (*Weekday)(nil)

    // read/write from/to sql
    _ sql.Scanner   = (*Weekday)(nil)
    _ driver.Valuer = (*Weekday)(nil)
)

// MarshalJSON marshals the enum as a quoted json string
func (w Weekday) MarshalJSON() ([]byte, error) {
    return []byte(`"` + w.String() + `"`), nil
}

func (w Weekday) MarshalText() (text []byte, err error) {
    return []byte(w.String()), nil
}

func (w *Weekday) UnmarshalJSON(b []byte) error {
    return w.UnmarshalText(b)
}

func (w *Weekday) UnmarshalText(b []byte) error {
    var dayName string
    if err := json.Unmarshal(b, &dayName); err != nil {
        return err
    }

    d, err := ParseWeekday(dayName)
    if err != nil {
        return err
    }

    *w = d
    return nil
}

// Value is used for sql exec to persist this type as a string
func (w Weekday) Value() (driver.Value, error) {
    return w.String(), nil
}

// Scan implements sql.Scanner so that Scan will be scanned correctly from storage
func (w *Weekday) Scan(src interface{}) error {
    switch t := src.(type) {
    case int:
        *w = Weekday(t)
    case int64:
        *w = Weekday(int(t))
    case string:
        d, err := ParseWeekday(t)
        if err != nil {
            return err
        }
        *w = d
    case []byte:
        d, err := ParseWeekday(string(t))
        if err != nil {
            return err
        }
        *w = d
    default:
        return errors.New("Weekday.Scan requires a string or byte array")
    }
    return nil
}


请注意,var块只是强制你正确实现方法,否则它将无法编译。
还要注意,如果你排除了MarshalJSON,则go将使用MarshalText(如果存在),因此,如果你只想让键具有自定义的marshal行为,但对值保持默认行为,则不应在主类型上使用这些方法,而应该有一个包装类型,仅用于映射键。
type MyType struct{}
type MyTypeKey MyType
var (
    // read/write from/to json keys
    _ encoding.TextMarshaler   = (*MyTypeKey)(nil)
    _ encoding.TextUnmarshaler = (*MyTypeKey)(nil)
)
func (w MyTypeKey) MarshalText() (text []byte, err error) {
    return []byte(w.String()), nil
}

func (w *MyTypeKey) UnmarshalText(b []byte) error {
    *w = MyTypeKey(ParseMyType(string(b)))
    return nil
}

欢迎改进这个答案,我希望其他人也能从中受益,同时我也希望下次我再需要时能够找到它 :)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接