英文:
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
。
清理并重新编组数据
现在,因为我们的Random
和Statuses
字段被标记为在JSON编组时忽略(json:"-"
),所以当你想要从这些类型输出原始JSON时,编组这个Person
类型将不起作用。最好将这个逻辑封装在一个自定义的JSON(解)编组器接口中。你可以在MarshalJSON
和UnmarshalJSON
方法中使用一些中间类型,或者只需创建一个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.Marshal
和json.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
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论