Adding Arbitrary fields to json output of an unknown struct

人走茶凉 提交于 2019-12-06 23:30:31
icza

There's an important difference between printInterface() and printStructPointer(). The first one embeds an interface type, while the second embeds a struct type (more specifically a pointer to a struct type).

When you embed a struct (or pointer to struct) type, the fields of the embedded type get promoted, so in the 2nd example it will be valid to write example2.Name. When you embed an interface type, an interface does not have fields, so no fields will be promoted. So it doesn't matter if the interface value wraps a struct (or pointer to struct), fields of that struct won't get promoted (they can't be).

Thus, in the printInterface() the interface wrapping a struct won't get "flattened" in the JSON result.

Solving it with generating a dynamic type using reflection

One way to solve this is to generate a dynamic type at runtime, using reflection (reflect package). This new type will be a struct, and it will contain an anonymous struct field being of the type that is wrapped in the passed interface, and will also contain our extra field (of type string).

This is how it could look like:

func printInterface(val interface{}) {
    t2 := reflect.StructOf([]reflect.StructField{
        reflect.StructField{
            Name: "",
            Type: reflect.TypeOf(val),
        },
        reflect.StructField{
            Name: "Extra",
            Type: reflect.TypeOf(""),
        },
    })

    v2 := reflect.New(t2).Elem()
    v2.Field(0).Set(reflect.ValueOf(val))
    v2.FieldByName("Extra").SetString("text")

    json.NewEncoder(os.Stdout).Encode(v2.Interface())
}

Output is as expected (try it on the Go Playground):

Example 1:
{"Name":"name","Extra":"text"}
Example 2:
{"Name":"name","Extra":"text"}

Solving it with marshaling twice

Another way would be to marshal the value, unmarshal it into a map, add the extra field and marshal it again:

func printInterface(val interface{}) error {
    data, err := json.Marshal(val)
    if err != nil {
        return err
    }

    v2 := map[string]interface{}{}
    if err := json.Unmarshal(data, &v2); err != nil {
        return err
    }

    v2["Extra"] = "text"
    return json.NewEncoder(os.Stdout).Encode(v2)
}

Output is the same. Try it on the Go Playground.

This solution is simpler, easier to follow, but it's slower as it marshals twice. Also note that in this example the fields in the result might be in different order, as iteration order on a map is not specified in Go (for details see Why can't Go iterate maps in insertion order?).

Here's one way:

package main

import (
    "encoding/json"
    "fmt"
    "os"
)

type example interface{}
type Data struct {
    Name string
}

func printInterface(val interface{}) {
    if d, ok := val.(*Data); ok {
        fmt.Println("Asserting type *Data for val is OK")
        printStructPointer(d)
    } else {
        fmt.Println("Asserting type *Data for val is NOT OK")
    }

}

func printStructPointer(val *Data) {

    example2 := struct {
        *Data
        Extra string
    }{
        Data:  val,
        Extra: "text",
    }
    json.NewEncoder(os.Stdout).Encode(example2)
}

func main() {
    d := Data{Name: "Testing"}
    fmt.Println("Example 1:")
    printInterface(&d)
    fmt.Println("Example 2:")
    printStructPointer(&d)
}

Playground: https://play.golang.org/p/OPotpTyUEz

You can also possibly use a type switch for an assertion, esp if you have many types. Hope this helps!

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!