简体   繁体   English

如何在 JSON 中自定义编组映射键

[英]How to custom marshal map keys in JSON

I can't understand a strange behavior of custom marshal int to string .我无法理解自定义编组intstring的奇怪行为。

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 https://play.golang.org/p/iiUyL2Hc5h_P

What am I missing?我错过了什么?

This is the expected outcome, which is documented at json.Marshal() :这是预期的结果,记录在json.Marshal()中:

Map values encode as JSON objects.映射值编码为 JSON 对象。 The map's key type must either be a string, an integer type, or implement encoding.TextMarshaler.映射的键类型必须是字符串、整数类型或实现 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:通过应用以下规则对映射键进行排序并用作 JSON 对象键,但要遵守上面为字符串值描述的 UTF-8 强制:

 - 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).请注意,映射键的处理方式与属性值的处理方式不同,因为 JSON 中的映射键是始终为string值的属性名称(而属性值可能是 JSON 文本、数字和布尔值)。

As per the doc, if you want it to work for map keys as well, implement encoding.TextMarshaler :根据文档,如果您希望它也适用于地图键,请实现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!) (注意MarshalText()应该返回“只是”简单的文本,而不是 JSON 文本,因此我们在其中省略了 JSON 编组!)

With this, output will be (try it on the Go Playground ):有了这个,输出将是(在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.请注意, encoding.TextMarshaler就足够了,因为在编组为值时也会检查它,而不仅仅是映射键。 So you don't have to implement both encoding.TextMarshaler and json.Marshaler .所以你不必同时实现encoding.TextMarshalerjson.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:如果你同时实现了这两个,当值被封送为“简单”值和映射键时,你可以有不同的输出,因为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
}

This time the output will be (try it on the Go Playground ):这次输出将是(在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}

The accepted answer is great but I've had to re-search for this enough times that I wanted to put a complete answer regarding marshal/unmarshal with examples so next time I can just copy paste as a starting point :)接受的答案很好,但我不得不重新搜索足够多的时间,我想用示例给出关于编组/解组的完整答案,所以下次我可以复制粘贴作为起点:)

The things I often search for include:我经常搜索的内容包括:

  • encode custom type to sql database将自定义类型编码到 sql 数据库
  • json encode enum int as string json 将枚举 int 编码为字符串
  • json encode map key but not value json编码映射键但不是值

In this example I make a custom Weekday type which matches time.Weekday int values but allows for the string value in request/response json and in the database在此示例中,我创建了一个自定义 Weekday 类型,它匹配 time.Weekday int 值,但允许请求/响应 json 和数据库中的字符串值

This same thing can be done with any int enum using iota to have the human readable value in json and database任何使用 iota 的 int 枚举都可以完成同样的事情,以便在 json 和数据库中具有人类可读的值

Playground example with tests : https://go.dev/play/p/aUxxIJ6tY9K带有测试的游乐场示例https ://go.dev/play/p/aUxxIJ6tY9K

The important bit is here though:重要的一点在这里:

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
}

Note that the var block simply forces you to implement the methods correctly else it won't compile.请注意, var 块只是强制您正确实现方法,否则它将无法编译。

Also note that if you exclude MarshalJSON then go will use MarshalText if it is there, so if you want only the key to have a custom marshal but to have the default behavior for the value then you should not use these methods on your main type, but instead have a wrapper type that you only use for map keys另请注意,如果您排除MarshalJSON则 go 将使用MarshalText如果它存在,因此如果您只希望键具有自定义封送但具有值的默认行为,那么您不应该在您的主要类型上使用这些方法,而是有一个仅用于映射键的包装器类型

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
}

Feel free to improve this answer, I hope others find it helpful and I hope I can find it again next time I want this and search for it again myself :)随意改进这个答案,我希望其他人觉得它有帮助,我希望下次我想要这个时我能再次找到它并自己再次搜索它:)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM