dtracers
dtracers

Reputation: 1648

Manually create json object from struct in golang

I have a struct lets say

type Foo struct {
  A string `json:",omitemtpy"
}

And I know that I can convert it to json easily with something like

json.Marshal(Foo{})

and it will respond with an empty json string.

But I need to return a json representation of the struct with all fields and "empty values" present within the json using the same struct. (in practice it is a very large struct so I can't just keep a copy without the tags)

What is the easiest way to do this?

Basically I need to create a json marshal of the struct that ignores the json omitempty tags.

This json creation does not need to be efficient or performant.

I would prefer if there is a library that exists for this type of task but most I have seen either create some special format or they respect omitempty

Edit:

Selected https://stackoverflow.com/a/77799949/2187510 as my answer with some additional work to allow for default values (using their code as reference)

defaultFoo := FoodWithPts{ Str: "helloWorld"}
dupFooType := dupType(reflect.TypeOf(defaultFoo))
foo := reflect.Zero(dupFooType).Interface()

// New additions
defaults, _ := json.Marshal(defaultFoo)
json.Unmarshal(defaults, &foo)   // overwrites foo with defaults
// End New additions

data, err := json.Marshal(foo)
fmt.Println("dup FooWithPtrs:\n", string(data), err)

Output:

dup FooWithPtrs:
    {"String":"helloWorld","Int":0,"Bar":null} <nil>

Upvotes: 3

Views: 599

Answers (1)

icza
icza

Reputation: 417662

You can't modify tags at runtime, but you can create struct types at runtime using reflect.StructOf().

So the idea is to duplicate the struct type, but in the duplicate exclude the ,omitempty option from JSON tags.

You can find all below examples on the Go Playground.

This is easier than one would think at first. We just have to do it recursively (a struct field may be another struct), and we definitely should handle pointers:

func dupType(t reflect.Type) reflect.Type {
    if t.Kind() == reflect.Pointer {
        return reflect.PointerTo(dupType(t.Elem()))
    }

    if t.Kind() != reflect.Struct {
        return t
    }

    var fields []reflect.StructField

    for i := 0; i < t.NumField(); i++ {
        sf := t.Field(i)
        sf.Type = dupType(sf.Type)
        // Keep json tag but cut ,omitempty option if exists:
        if tag, _ := strings.CutSuffix(sf.Tag.Get("json"), ",omitempty"); tag == "" {
            sf.Tag = ""
        } else {
            sf.Tag = `json:"` + reflect.StructTag(tag) + `"`
        }
        fields = append(fields, sf)
    }

    return reflect.StructOf(fields)
}

Let's test it with this type:

type Foo struct {
    Str string `json:"String,omitempty"`
    Int int    `json:",omitempty"`
    Bar struct {
        Float  float64 `json:",omitempty"`
        PtrInt int     `json:",omitempty"`
        Baz    struct {
            X int `json:"XXXX,omitempty"`
        } `json:",omitempty"`
    } `json:",omitempty"`
}

First this is what the JSON output would be without type duplication:

data, err := json.Marshal(Foo{})
fmt.Println("Foo:\n", string(data), err)

Output:

Foo:
 {"Bar":{"Baz":{}}} <nil>

Note that we got Bar and Baz fields as those are structs.

Let's try with type duplication:

dupFooType := dupType(reflect.TypeOf(Foo{}))
foo := reflect.Zero(dupFooType).Interface()

data, err := json.Marshal(foo)
fmt.Println("dup Foo:\n", string(data), err)

This will output:

dup Foo:
 {"String":"","Int":0,"Bar":{"Float":0,"PtrInt":0,"Baz":{"XXXX":0}}} <nil>

Nice! Exactly what we want!

But we're not done yet. What if we have a type with struct pointer fields? Like this:

type FooWithPtrs struct {
    Str string `json:"String,omitempty"`
    Int int    `json:",omitempty"`
    Bar *struct {
        Float  float64 `json:",omitempty"`
        PtrInt int     `json:",omitempty"`
        Baz    *struct {
            X int `json:"XXXX,omitempty"`
        } `json:",omitempty"`
    } `json:",omitempty"`
}

Attempting to JSON-marshal a value of the duplicated type:

dupFooType := dupType(reflect.TypeOf(FooWithPtrs{}))
foo := reflect.Zero(dupFooType).Interface()

data, err := json.Marshal(foo)
fmt.Println("dup FooWithPtrs:\n", string(data), err)

Output:

dup FooWithPtrs:
 {"String":"","Int":0,"Bar":null} <nil>

If the struct contains pointers, those appear in the JSON output as null, but we would want their fields too in the output. That requires initializing them to non-nil values so they generate output.

Luckily we can also do this using reflection:

func initPtrs(v reflect.Value) {
    if !v.CanAddr() {
        return
    }

    if v.Kind() == reflect.Pointer {
        v.Set(reflect.New(v.Type().Elem()))
        v = v.Elem()
    }

    if v.Kind() == reflect.Struct {
        for i := 0; i < v.NumField(); i++ {
            initPtrs(v.Field(i))
        }
    }
}

We're excited! Let's see this in action:

dupFooType := dupType(reflect.TypeOf(FooWithPtrs{}))
fooVal := reflect.New(dupFooType)
initPtrs(fooVal.Elem())

data, err := json.Marshal(fooVal.Interface())
fmt.Println("dup and inited FooWithPtrs:\n", string(data), err)

Output:

dup and inited FooWithPtrs:
 {"String":"","Int":0,"Bar":{"Float":0,"PtrInt":0,"Baz":{"XXXX":0}}} <nil>

Nice! It contains all fields!

Upvotes: 2

Related Questions