动态键的JSON反序列化

huangapple go评论87阅读模式
英文:

Unmarshalling of JSON with dynamic keys

问题

我有一个场景,其中包含一个具有动态字段集的 JSON,需要将其解组为一个结构体。

const jsonStream = `{
    "name": "john",
    "age": 23,
    "bvu62fu6dq": {
        "status": true
    }
}`

type Status struct {
    Status bool
}

type Person struct {
    Name   string            `json:"name"`
    Age    int               `json:"age"`
    Status map[string]Status `json:"status"`
}

func main() {
    dec := json.NewDecoder(strings.NewReader(jsonStream))
    for {
        var person Person
        if err := dec.Decode(&person); err == io.EOF {
            break
        } else if err != nil {
            log.Fatal(err)
        }
        fmt.Println(person)
        fmt.Println(person.Status["bvu62fu6dq"])
    }
}

输出结果:

{john 23 map[]}
{false}

当解组时,嵌套的 status 结构体无法正确解析为 JSON 中的值(即使 JSON 中的值为 true,也显示为 false),代码中是否存在问题?

英文:

I have a scenario where the JSON that has dynamic set of fields that need to get unmarshalled in to a struct.

const jsonStream = `{
    "name": "john",
    "age": 23,
    "bvu62fu6dq": {
        "status": true
    }
}`

type Status struct {
    Status bool
}

type Person struct {
    Name   string            `json:"name"`
    Age    int               `json:"age"`
    Status map[string]Status `json:"status"`
}

func main() {
    dec := json.NewDecoder(strings.NewReader(jsonStream))
    for {
	    var person Person
	    if err := dec.Decode(&person); err == io.EOF {
		    break
	    } else if err != nil {
		    log.Fatal(err)
	    }
	    fmt.Println(person)
	    fmt.Println(person.Status["bvu62fu6dq"])
    }
}

The output:

{john 23 map[]}
{false}

When it gets unmarshalled, the nested status struct is not being correctly resolved to the value in the JSON (shows false even with true value in JSON), is there any issue in the code?

答案1

得分: 4

你的类型与你拥有的JSON不太匹配:

type Status struct {
    Status bool
}

type Person struct {
    Name   string            `json:"name"`
    Age    int               `json:"age"`
    Status map[string]Status `json:"status"`
}

对应的JSON大致如下:

{
    "name": "foo",
    "age": 12,
    "status": {
        "some-string": {
            "Status": true
        }
    }
}

在Go类型中,如果要解组具有已知/未知字段混合的数据,最简单的方法是使用以下方式:

type Person struct {
    Name   string                 `json:"name"`
    Age    int                    `json:"age"`
    Random map[string]interface{} `json:"-"`
}

首先解组已知数据:

var p Person
if err := json.Unmarshal([]byte(jsonStream), &p); err != nil {
    panic(err)
}

然后解组剩余的数据:

if err := json.Unmarshal([]byte(jsonStream), &p.Random); err != nil {
    panic(err)
}

现在,Random map将包含所有的未知键和它们的值。这些值显然将是一个具有status字段的对象,该字段应该是一个布尔值。你可以使用类型断言将它们转换为更合适的类型,或者你可以采取捷径,将它们进行编组/解组。更新你的Person类型如下:

type Person struct {
    Name     string                 `json:"name"`
    Age      int                    `json:"age"`
    Random   map[string]interface{} `json:"-"`
    Statuses map[string]Status      `json:"-"`
}

然后,将清理后的Random值进行编组和解组,存入Statuses字段:

b, err := json.Marshal(p.Random)
if err != nil {
    panic(err)
}
if err := json.Unmarshal(b, &p.Statuses); err != nil {
    panic(err)
}
// 删除Random map
p.Random = nil

结果是Person.Statuses["bvu62fu6dq"].Status被设置为true

清理并重新编组数据

现在,因为我们的RandomStatuses字段被标记为在JSON编组时忽略(json:"-"),所以当你想要从这些类型输出原始JSON时,编组这个Person类型将不起作用。最好将这个逻辑封装在一个自定义的JSON(解)编组器接口中。你可以在MarshalJSONUnmarshalJSON方法中使用一些中间类型,或者只需创建一个map并设置所需的键:

func (p Person) MarshalJSON() ([]byte, error) {
    data := make(map[string]interface{}, len(p.Statuses)+2) // 2是额外的字段
    // 复制status字段
    for k, v := range p.Statuses {
        data[k] = v
    }
    // 添加已知键
    data["name"] = p.Name
    data["age"] = p.Age
    return json.Marshal(data) // 返回编组后的map
}

类似地,你可以对UnmarshalJSON做同样的事情,但是你需要创建一个没有自定义处理的Person类型的版本:

type intermediaryPerson struct {
    Name   string                 `json:"name"`
    Age    int                    `json:"age"`
    Random map[string]interface{} `json:"-"`
}

// 不再需要标签和辅助字段
type Person struct {
    Name     string            `json:"name"`
    Age      int               `json:"age"`
    Statuses map[string]Status `json:"-"`
}

func (p *Person) UnmarshalJSON(data []byte) error {
    i := intermediaryPerson{}
    if err := json.Unmarshal(data, &i); err != nil {
        return err
    }
    if err := json.Unmarshal(data, &i.Random); err != nil {
        return err
    }
    delete(i.Random, "name")
    delete(i.Random, "age")
    stat, err := json.Marshal(i.Random)
    if err != nil {
        return err
    }
    // 复制已知字段
    p.Name = i.Name
    p.Age = i.Age
    return json.Unmarshal(stat, &p.Statuses) // 设置status字段
}

在这种情况下,通常会创建一个处理已知字段的类型并将其嵌入其中,然后在中间类型和“主要”/导出类型中嵌入它:

type BasePerson struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

type interPerson struct {
    BasePerson
    Random   map[string]interface{} `json:"-"`
}

type Person struct {
    BasePerson
    Statuses map[string]Status
}

这样,你可以直接将已知字段解组到BasePerson类型中,然后处理map:

func (p *Person) UnmarshalJSON(data []byte) error {
    base := BasePerson{}
    if err := json.Unmarshal(data, &base); err != nil {
        return err
    }
    p.BasePerson = base // 处理所有已知字段
    unknown := map[string]interface{}{}
    if err := json.Unmarshal(data, &unknown); err != nil {
        return err
    }
    // 处理status字段,与之前相同
    delete(unknown, "name") // 删除已知字段
    // 编组未知键map,然后解组到p.Statuses中
}

这就是我会处理的方式。它使得调用json.Marshaljson.Unmarshal看起来就像处理任何其他类型一样,它将未知字段的处理集中在一个地方(编组器/解编组器接口的实现),并且让你拥有一个单一的Person类型,其中每个字段都包含所需的数据,以可用的格式。它在某种程度上效率不高,因为它依赖于对未知键进行解编组/编组/解编组。你可以通过使用类型断言并在unknown map上进行迭代来消除这一点,像这样:

for k, v := range unknown {
    m, ok := v.(map[string]interface{})
    if !ok {
        continue // 不是{"status": bool}
    }
    s, ok := m["status"]
    if !ok {
        continue // status键不存在,忽略
    }
    if sb, ok := s.(bool); ok {
        // 好的,我们有一个status布尔值
        p.Statuses[k] = Status{
            Status: sb,
        }
    }
}

但实话实说,性能差异不会太大(这是微优化),而且代码有点冗长。懒一点,需要时再进行优化,而不是随时随地进行优化。

英文:

Your types don't really match with the JSON you have:

type Status struct {
    Status bool
}

type Person struct {
    Name   string            `json:"name"`
    Age    int               `json:"age"`
    Status map[string]Status `json:"status"`
}

Maps to JSON that looks something like this:

{
    "name": "foo",
    "age": 12,
    "status": {
        "some-string": {
            "Status": true
        }
    }
}

The easiest way to unmarshal data with a mix of known/unknown fields in a go type is to have something like this:

type Person struct {
    Name   string                 `json:"name"`
    Age    int                    `json:"age"`
    Random map[string]interface{} `json:"-"` // skip this key
}

Then, first unmarshal the known data:

var p Person
if err := json.Unmarshal([]byte(jsonStream), &p); err != nil {
    panic(err)
}
// then unmarshal the rest of the data
if err := json.Unmarshal([]byte(jsonStream), &p.Random); err != nil {
    panic(err)
}

Now the Random map will contain every and all data, including the name and age fields. Seeing as you've got those tagged on the struct, these keys are known, so you can easily delete them from the map:

delete(p.Random, "name")
delete(p.Random, "age")

Now p.Random will contain all the unknown keys and their respective values. These values apparently will be an object with a field status, which is expected to be a boolean. You can set about using type assertions and convert them all over to a more sensible type, or you can take a shortcut and marshal/unmarshal the values. Update your Person type like so:

type Person struct {
	Name     string                 `json:"name"`
    Age      int                    `json:"age"`
    Random   map[string]interface{} `json:"-"`
    Statuses map[string]Status      `json:"-"`
}

Now take the clean Random value, marshal it and unmarshal it back into the Statuses field:

b, err := json.Marshal(p.Random)
if err != nil {
    panic(err)
}
if err := json.Unmarshal(b, &p.Statuses); err != nil {
    panic(err)
}
// remove Random map
p.Random = nil

The result is Person.Statuses["bvu62fu6dq"].Status is set to true

Demo


Cleaning this all up, and marshalling the data back

Now because our Random and Statuses fields are tagged to be ignored for JSON marshalling (json:"-"), marshalling this Person type won't play nice when you want to output the original JSON from these types. It's best to wrap this logic up in a custom JSON (un)-Marshaller interface. You can either use some intermediary types in your MarshalJSON and UnmarshalJSON methods on the Person type, or just create a map and set the keys you need:

func (p Person) MarshalJSON() ([]byte, error) {
    data := make(map[string]interface{}, len(p.Statuses) + 2) // 2 being the extra fields
    // copy status fields
    for k, v := range p.Statuses {
        data[k] = v
    }
    // add known keys
    data["name"] = p.Name
    data["age"] = p.Age
    return json.Marshal(data) // return the marshalled map
}

Similarly, you can do the same thing for UnmarshalJSON, but you'll need to create a version of the Person type that doesn't have the custom handling:

type intermediaryPerson struct {
    Name string  `json:"name"`
    Age  int `json:"age"`
    Random map[string]interface{} `json:"-"`
}

// no need for the tags and helper fields anymore
type Person struct {
    Name    string
    Age     int
    Statuses map[string]Status // Status type doesn't change
}

func (p *Person) UnmarshalJSON(data []byte) error {
    i := intermediaryPerson{}
    if err := json.Unmarshal(data, &i); err != nil {
        return err
    }
    if err := json.Unmarshal(data, &i.Random); err != nil {
        return err
    }
    delete(i.Random, "name")
    delete(i.Random, "age")
    stat, err := json.Marshal(i.Random)
    if err != nil {
        return err
    }
    // copy known fields
    p.Name = i.Name
    p.Age = i.Age
    return json.Unmarshal(stat, &p.Statuses) // set status fields
}

In cases like this, it's common to create a type that handles the known fields and embed that, though:

type BasePerson struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

and embed that in both the intermediary and the "main"/exported type:

type interPerson struct {
   BasePerson
   Random map[string]interface{} `json:"-"`
}

type Person struct {
    BasePerson
    Statuses map[string]Status
}

That way, you can just unmarshal the known fields directly into the BasePerson type, assign it, and then handle the map:

func (p *Person) UnmarshalJSON(data []byte) error {
    base := BasePerson{}
    if err := json.Unmarshal(data, &base); err != nil {
        return err
    }
    p.BasePerson = base // takes care of all known fields
    unknown := map[string]interface{}{}
    if err := json.Unmarshal(data, unknown); err != nil {
        return err
    }
    // handle status stuff same as before
    delete(unknown, "name") // remove known fields
    // marshal unknown key map, then unmarshal into p.Statuses
}

Demo 2

This is how I'd go about it. It allows for calls to json.Marshal and json.Unmarshal to look just like any other type, it centralises the handling of unknown fields in a single place (the implementation of the marshaller/unmarshaller interface), and leaves you with a single Person type where every field contains the required data, in a usable format. It's a tad inefficient in that it relies on unmarshalling/marshalling/unmarshalling the unknown keys. You could do away with that, like I said, using type assertions and iterating over the unknown map instead, faffing around with something like this:

for k, v := range unknown {
    m, ok := v.(map[string]interface{})
    if !ok {
        continue // not {"status": bool}
    }
    s, ok := m["status"]
    if !ok {
        continue // status key did not exist, ignore
    }
    if sb, ok := s.(bool); ok {
        // ok, we have a status bool value
        p.Statuses[k] = Status{
            Status: sb,
        }
    }
}

But truth be told, the performance difference won't be that great (it's micro optimisation IMO), and the code is a tad too verbose to my liking. Be lazy, optimise when needed, not whenever

答案2

得分: -1

类型与您的JSON值不匹配。

    const jsonStream = `{
    "name": "john",
    "age": 23,
    "bvu62fu6dq": {
        "status": true
     }
   }`

对于上述JSON,您的代码应该如下所示才能正常工作(对现有代码进行了一些修改)。

package main

import (
	"encoding/json"
	"fmt"
	"io"
	"log"
	"strings"
)

const jsonStream = `{
    "name": "john",
    "age": 23,
    "bvu62fu6dq": {
        "status": true
    }
}`

type bvu62fu6dq struct {
	Status bool
}

type Person struct {
	Name   string     `json:"name"`
	Age    int        `json:"age"`
	Status bvu62fu6dq `json:"bvu62fu6dq"`
}

func main() {
	dec := json.NewDecoder(strings.NewReader(jsonStream))
	for {
		var person Person
		if err := dec.Decode(&person); err == io.EOF {
			break
		} else if err != nil {
			log.Fatal(err)
		}
		fmt.Println(person)
		fmt.Println(person.Status)
	}
}

根据您的JSON数据,您需要将其映射到类型字段。
运行代码片段

英文:

Type doesn't meet with your json value.

    const jsonStream = `{
    "name": "john",
    "age": 23,
    "bvu62fu6dq": {
        "status": true
     }
   }`

For above json your code should look like below snnipet to work (some modifications in your existing code).

package main

import (
	"encoding/json"
	"fmt"
	"io"
	"log"
	"strings"
)

const jsonStream = `{
    "name": "john",
    "age": 23,
    "bvu62fu6dq": {
        "status": true
    }
}`

type bvu62fu6dq struct {
	Status bool
}

type Person struct {
	Name   string     `json:"name"`
	Age    int        `json:"age"`
	Status bvu62fu6dq `json:"bvu62fu6dq"`
}

func main() {
	dec := json.NewDecoder(strings.NewReader(jsonStream))
	for {
		var person Person
		if err := dec.Decode(&person); err == io.EOF {
			break
		} else if err != nil {
			log.Fatal(err)
		}
		fmt.Println(person)
		fmt.Println(person.Status)
	}
}

Based on your json data you have to map with type fields.
Run code snippet

huangapple
  • 本文由 发表于 2022年6月13日 23:39:49
  • 转载请务必保留本文链接:https://go.coder-hub.com/72605521.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定